Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ csv = "1.4.0"
eyre = "0.6.12"
indicatif = "0.18.3"
litesvm = "0.8.2"
magnus-router-client = { git = "https://git@github.com/limechain/magnus.git", tag = "0.0.1-rc" }
magnus-shared = { git = "https://git@github.com/limechain/magnus.git", tag = "0.0.1-rc" }
magnus-router-client = { git = "https://git@github.com/limechain/magnus.git", rev = "24958ce" }
magnus-shared = { git = "https://git@github.com/limechain/magnus.git", rev = "24958ce" }
secrecy = "0.10.3"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
Expand Down
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# pmm-sim

Simulation & Benchmark environment for Solana's Proprietary AMMs. The setup relies on [Litesvm](https://crates.io/crates/litesvm) for local, consistent and expedited execution. Additionally, since some proprietary AMMs block swaps originating from direct offchain calls, we rely on a custom router program - [Magnus](https://github.com/limechain/magnus) - to facilitate the swap execution.

![Demo](./assets/demo.gif)
Simulation & Benchmark environment for Solana's Proprietary AMMs. The setup relies on [Litesvm](https://crates.io/crates/litesvm) for local, consistent and expedited execution. Additionally, since some proprietary AMMs block swaps originating from direct offchain calls, we rely on a custom router program - [magnus-router](https://github.com/LimeChain/magnus/tree/master/crates/router) - to facilitate the swap execution.

Supported Prop AMMs:

Expand All @@ -12,6 +10,7 @@ Supported Prop AMMs:
- [x] ZeroFi
- [x] TesseraV
- [x] GoonFi
- [x] BisonFi

The swaps can be done either with the local static accounts that can be found at [cfg/accounts](./cfg/accounts) or with the current live accounts (by fetching them on-the-go). By default all swaps & benchmark simulations are done with live accounts. The markets are specified in [setup.toml](./setup.toml).

Expand Down Expand Up @@ -40,19 +39,19 @@ cargo build --release

### Single-route swaps

##### Swap 15K USDC for WSOL using Humidifi.
##### Swap 15K USDC for WSOL using HumidiFi.

```
./target/release/pmm-sim single --amount-in=15000 --pmms=humidifi --weights=100 --src-token=USDC --dst-token=WSOL
```

##### Swap 375 WSOL for USDC using Tessera and SolfiV2, in one route, split evenly - 187,5 WSOL per Prop AMM.
##### Swap 375 WSOL for USDC using Tessera and SolFiV2, in one route, split evenly - 187,5 WSOL per Prop AMM.

```
./target/release/pmm-sim single --pmms=tessera,solfi-v2 --weights=50,50 --amount-in=375 --src-token=WSOL --dst-token=USDC
```

##### Swap 100 WSOL for USDC using SolfiV2, Humidifi, and Tessera, in one route, split 33,33,34 WSOL per Prop AMM.
##### Swap 100 WSOL for USDC using SolFiV2, HumidiFi, and Tessera, in one route, split 33,33,34 WSOL per Prop AMM.

```
./target/release/pmm-sim single --amount-in=100 --pmms=solfi-v2,humidifi,tessera --weights=33,33,34 --src-token=WSOL --dst-token=USDC --jit-accounts=false
Expand All @@ -66,33 +65,33 @@ cargo build --release

### Multi-route swaps

##### Swap 103 WSOL for USDC in a multi-route swap, 100 WSOL via Humidifi and SolfiV2 (split 92%/8%) in one route, and 3 WSOL via Tessera in another route.
##### Swap 103 WSOL for USDC in a multi-route swap, 100 WSOL via HumidiFi and SolFiV2 (split 92%/8%) in one route, and 3 WSOL via Tessera in another route.

```
./target/release/pmm-sim multi --pmms="[[humidifi,solfi-v2],[tessera]]" --weights="[[92, 8],[100]]" --amount-in=100,3
```

##### Execute two routes, the first swapping 150,000 USDC for WSOL using Humidifi and SolfiV2 (split 25%/75%), the second swapping 1000 USDC for WSOL using Goonfi.
##### Execute two routes, the first swapping 150,000 USDC for WSOL using HumidiFi and SolFiV2 (split 25%/75%), the second swapping 1000 USDC for WSOL using GoonFi.

```
RUST_LOG=debug ./target/release/pmm-sim multi --pmms="[[humidifi,solfi-v2],[goonfi]]" --weights="[[25,75],[100]]" --amount-in=150000,1000 --src-token=USDC --dst-token=WSOL --jit-accounts=true
```

### Benchmark swaps

##### Benchmark swaps on Humidifi,Tessera,SolfiV2 and Goonfi, from 1 to 4000 WSOL to USDC, in increments of 1 WSOL. The results are saved at [./datasets](./datasets).
##### Benchmark swaps on HumidiFi,Tessera,SolFiV2 and GoonFi, from 1 to 4000 WSOL to USDC, in increments of 1 WSOL. The results are saved at [./datasets](./datasets).

```
./target/release/pmm-sim benchmark --range=1.0,4000.0,1.0 --pmms=humidifi,tessera,solfi-v2,goonfi --src-token=wsol --dst-token=usdc
```

##### Benchmark swaps on Tessera and SolfiV2, from 1 to 250 WSOL, in increments of 0.01 WSOL. The results are saved at [./datasets](./datasets).
##### Benchmark swaps on Tessera and SolFiV2, from 1 to 250 WSOL, in increments of 0.01 WSOL. The results are saved at [./datasets](./datasets).

```
./target/release/pmm-sim benchmark --range=1.0,250.0,0.01 --pmms=tessera,solfi-v2 --src-token=wsol --dst-token=usdc
```

##### Benchmark swaps (USDC->WSOL) on Humidifi and SolfiV2, from 10K to 100K USDC, in increments of 100 USDC. The results are saved at [./datasets](./datasets).
##### Benchmark swaps (USDC->WSOL) on HumidiFi and SolFiV2, from 10K to 100K USDC, in increments of 100 USDC. The results are saved at [./datasets](./datasets).

```
./target/release/pmm-sim benchmark --range=10000,100000,100 --pmms=humidifi,solfi-v2 --src-token=usdc --dst-token=wsol
Expand All @@ -114,7 +113,7 @@ Generated benchmark data can be plotted through [./scripts/plot.py](./scripts/pl
./target/release/pmm-sim fetch-accounts
```

##### Locally sync the current (live) accounts for Humidifi and SolfiV2.
##### Locally sync the current (live) accounts for HumidiFi and SolFiV2.

```
./target/release/pmm-sim fetch-accounts --pmms=humidifi,solfi-v2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"account": {
"data": [
"UE9PTFNUQVQCAAAAAAAAAAUEB2fzKaaE5W72FrNuMbIAofl22B4sDDaBkFYBNbNkOdYMWaUMAACCN6LoIgAAAIgiGFrFYJEAdBNzFwAAAAB0E3MXAAAAALT9Am92j4oYAgAEAAAAAADJwpuEVwIAABiAe+FOAAAA3ixUEcpWC5amPmeKCZJkFbQyPlxuclan9+CpmKDNU8FNdErq01xEcOfV3tA8AiFnOdR3aBdpghVGUEFu/yMB2gabiFf+q4GE+2h/Y0YYwDXaxDncGus7VZig8AAAAAABxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWH+DgAAAAAAAFNPTC1VU0RDLVBvb2wxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOIEAAD/////AAAAAAAAAADECQAA/v///wAAAAAAAAAAxAkAAP3///8AAAAAAAAAAMQJAAD8////AAAAAOIEAAAAAAAAAQAAAAAAAADECQAAAAAAAAIAAAAAAAAAxAkAAAAAAAADAAAAAAAAAMQJAAAAAAAABAAAAAAAAADiBAAA4gQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkGAAAAAAAAAAAAAAAAAADoAwAAAAAAAAAAAAAAAAAAAgABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"base64"
],
"executable": false,
"lamports": 15144983,
"owner": "BiSoNHVpsVZW2F7rx2eQ59yQwKxzU5NvBcmKshCSUypi",
"rentEpoch": 18446744073709551615
},
"pubkey": "51FQwjrvo8J8zXUaKyAznJ5NYpoiTCuqAqCu3HAMB9NZ",
"slot": 393417588
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"account": {
"data": [
"xvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWE7fwtZrcOTPb5IU1NauYoZ9NuJAjBaEe3Y/La8dZeucoI3ougiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"base64"
],
"executable": false,
"lamports": 2039280,
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"rentEpoch": 18446744073709551615
},
"pubkey": "6DMF4t6Ks8yXhG8K3rrTAeNYrqNrr1DwewHvBmH3a3FX",
"slot": 393417588
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"account": {
"data": [
"BpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAE7fwtZrcOTPb5IU1NauYoZ9NuJAjBaEe3Y/La8dZeucjnWDFmlDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAADwHR8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"base64"
],
"executable": false,
"lamports": 13904305189939,
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"rentEpoch": 18446744073709551615
},
"pubkey": "FxGiN5NkigicwrnFshZEAUH9C13yrBALmgYxA9x8sfnQ",
"slot": 393417588
}
Binary file added cfg/programs/bisonfi.so
Binary file not shown.
Binary file modified cfg/programs/magnus-router.so
Binary file not shown.
6 changes: 6 additions & 0 deletions setup.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,9 @@ reserve_y = "AAamGhyPfpQJWfZHTq944NM1cFvoVLDrQxt7HGjeRQUS"
ref_oracle = "J4HJYz4p7TRP96WVFky3vh7XryxoFehHjoRySUTeSeXw"
x_price_feed = "J4HJYz4p7TRP96WVFky3vh7XryxoFehHjoRySUTeSeXw"
y_price_feed = "J4HJYz4p7TRP96WVFky3vh7XryxoFehHjoRySUTeSeXw"

# https://solscan.io/account/51FQwjrvo8J8zXUaKyAznJ5NYpoiTCuqAqCu3HAMB9NZ
[bisonfi]
market = "51FQwjrvo8J8zXUaKyAznJ5NYpoiTCuqAqCu3HAMB9NZ"
market_base_ta = "6DMF4t6Ks8yXhG8K3rrTAeNYrqNrr1DwewHvBmH3a3FX"
market_quote_ta = "FxGiN5NkigicwrnFshZEAUH9C13yrBALmgYxA9x8sfnQ"
61 changes: 43 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ macro_rules! define_dex_configs {
// All DEX Cfgs
// Reference the cfg file — `setup.toml`
define_dex_configs! {
Humidifi => HumidifiCfg : humidifi ("humidifi") {
HumidiFi => HumidiFiCfg : humidifi ("humidifi") {
market,
base_ta,
quote_ta,
Expand All @@ -160,7 +160,7 @@ define_dex_configs! {
quote_ta,
global_state,
},
Goonfi => GoonfiCfg : goonfi ("goonfi") {
GoonFi => GoonFiCfg : goonfi ("goonfi") {
market,
base_ta,
quote_ta,
Expand All @@ -173,7 +173,7 @@ define_dex_configs! {
cfg,
oracle,
},
Zerofi => ZerofiCfg : zerofi ("zerofi") {
ZeroFi => ZeroFiCfg : zerofi ("zerofi") {
market,
vault_info_base,
vault_base,
Expand All @@ -190,6 +190,11 @@ define_dex_configs! {
x_price_feed,
y_price_feed,
},
BisonFi => BisonfiCfg : bisonfi ("bisonfi") {
market,
market_base_ta,
market_quote_ta,
},
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -349,7 +354,7 @@ pub enum Command {
about = "Benchmark swaps for any one of the implemented Prop AMMs by specifying, optionally, the accounts, src/dst tokens and \
step size",
after_help = "Examples:
# Benchmark swaps on Humidifi,Tessera,SolfiV2 and Goonfi, from 1 to 4000 WSOL to USDC, in increments of 1 WSOL.
# Benchmark swaps on HumidiFi,Tessera,SolfiV2 and GoonFi, from 1 to 4000 WSOL to USDC, in increments of 1 WSOL.
pmm-sim benchmark --range=1.0,4000.0,1.0 --pmms=humidifi,tessera,solfi-v2,goonfi --src-token=wsol --dst-token=usdc

Benchmark swaps on Tessera and SolfiV2, from 1 to 250 WSOL, in increments of 0.01 WSOL.
Expand Down Expand Up @@ -623,9 +628,9 @@ impl<'a, P: Into<String> + Display + Clone + Debug> Environment<'a, P> {

/// Fetches PMM accounts from RPC and warps to the fetched slot.
fn jit_accounts(&mut self, pmms: &[Dex], client: &RpcClient) -> eyre::Result<()> {
let (slot, fetched) = Misc::fetch_accounts(pmms, client, &self.cfg)?;
let (slot, accs_map) = Misc::fetch_accounts(pmms, client, &self.cfg)?;

fetched.iter().try_for_each(|(_, accs)| self.load_accounts(accs))?;
accs_map.iter().try_for_each(|(_, accs)| self.load_accounts(accs))?;

self.svm.warp_to_slot(slot);
self.slot = Some(slot);
Expand Down Expand Up @@ -678,7 +683,7 @@ impl<'a, P: Into<String> + Display + Clone + Debug> Environment<'a, P> {
.iter()
.find_map(|log| {
if log.contains("SwapEvent") {
// i.e.: "Program log: SwapEvent { dex: Humidifi, amount_in: 1000000000, amount_out: 121518066 }"
// i.e.: "Program log: SwapEvent { dex: HumidiFi, amount_in: 1000000000, amount_out: 121518066 }"
log.split("amount_out: ").nth(1)?.split(|c: char| !c.is_ascii_digit()).next()?.parse().ok()
} else {
None
Expand Down Expand Up @@ -721,12 +726,13 @@ impl<'a> ConstructSwap<'a> {
/// appropriate PMM-specific attachment function based on the DEX type.
fn attach_pmm_accs(&mut self, pmm: &Dex) {
match pmm {
Dex::Humidifi => self.attach_humidifi_accs(),
Dex::HumidiFi => self.attach_humidifi_accs(),
Dex::SolfiV2 => self.attach_solfiv2_accs(),
Dex::Zerofi => self.attach_zerofi_accs(),
Dex::ZeroFi => self.attach_zerofi_accs(),
Dex::ObricV2 => self.attach_obric_v2_accs(),
Dex::Tessera => self.attach_tessera_accs(),
Dex::Goonfi => self.attach_goonfi_accs(),
Dex::GoonFi => self.attach_goonfi_accs(),
Dex::BisonFi => self.attach_bisonfi_accs(),
_ => {
unimplemented!()
}
Expand Down Expand Up @@ -861,6 +867,25 @@ impl<'a> ConstructSwap<'a> {
AccountMeta::new_readonly(spl_token::id(), false),
]);
}

fn attach_bisonfi_accs(&mut self) {
let Some(cfg) = &self.cfg.bisonfi else {
panic!("BisonFi config is missing, cannot attach accounts.");
};

self.builder.add_remaining_accounts(&[
AccountMeta::new_readonly(Pubkey::new_from_array(magnus_shared::pmm_bisonfi::id().to_bytes()), false),
AccountMeta::new(self.payer, true),
AccountMeta::new(cfg.market, false),
AccountMeta::new(cfg.market_base_ta, false),
AccountMeta::new(cfg.market_quote_ta, false),
AccountMeta::new(self.src_ta, false),
AccountMeta::new(self.dst_ta, false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::instructions::id(), false),
]);
}
}

struct Misc;
Expand Down Expand Up @@ -1500,56 +1525,56 @@ mod tests {
fn test_parse_nested_pmms_json_single() {
let input = r#"[["humidifi"]]"#;
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi]]);
assert_eq!(result, vec![vec![Dex::HumidiFi]]);
}

#[test]
fn test_parse_nested_pmms_json_multiple() {
let input = r#"[["humidifi","obric-v2"],["zerofi"]]"#;
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi, Dex::ObricV2], vec![Dex::Zerofi]]);
assert_eq!(result, vec![vec![Dex::HumidiFi, Dex::ObricV2], vec![Dex::ZeroFi]]);
}

#[test]
fn test_parse_nested_pmms_no_quotes_single() {
let input = "[[humidifi]]";
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi]]);
assert_eq!(result, vec![vec![Dex::HumidiFi]]);
}

#[test]
fn test_parse_nested_pmms_no_quotes_single_route_multiple_pmms() {
let input = "[[humidifi,obric-v2]]";
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi, Dex::ObricV2]]);
assert_eq!(result, vec![vec![Dex::HumidiFi, Dex::ObricV2]]);
}

#[test]
fn test_parse_nested_pmms_no_quotes_multiple_routes() {
let input = "[[humidifi,obric-v2],[zerofi]]";
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi, Dex::ObricV2], vec![Dex::Zerofi]]);
assert_eq!(result, vec![vec![Dex::HumidiFi, Dex::ObricV2], vec![Dex::ZeroFi]]);
}

#[test]
fn test_parse_nested_pmms_no_quotes_three_routes() {
let input = "[[humidifi],[obric-v2,solfi-v2],[zerofi]]";
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi], vec![Dex::ObricV2, Dex::SolfiV2], vec![Dex::Zerofi],]);
assert_eq!(result, vec![vec![Dex::HumidiFi], vec![Dex::ObricV2, Dex::SolfiV2], vec![Dex::ZeroFi],]);
}

#[test]
fn test_parse_nested_pmms_no_quotes_all_pmms() {
let input = "[[raydium-cl-v2,raydium-cp],[obric-v2,solfi-v2,zerofi,humidifi]]";
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::RaydiumClV2, Dex::RaydiumCp], vec![Dex::ObricV2, Dex::SolfiV2, Dex::Zerofi, Dex::Humidifi],]);
assert_eq!(result, vec![vec![Dex::RaydiumClV2, Dex::RaydiumCp], vec![Dex::ObricV2, Dex::SolfiV2, Dex::ZeroFi, Dex::HumidiFi],]);
}

#[test]
fn test_parse_nested_pmms_no_quotes_with_spaces() {
let input = "[[ humidifi , obric-v2 ],[ zerofi ]]";
let result = CliArgs::parse_nested_pmms(input).unwrap();
assert_eq!(result, vec![vec![Dex::Humidifi, Dex::ObricV2], vec![Dex::Zerofi]]);
assert_eq!(result, vec![vec![Dex::HumidiFi, Dex::ObricV2], vec![Dex::ZeroFi]]);
}

#[test]
Expand Down