Skip to content
Open
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 crates/magnus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ num-derive = "0.4"
num-traits = "0.2"
num_cpus = "1.17.0"
oneshot = "0.1.11"
pyth-sdk = { git = "https://github.com/lbkolev/pyth-sdk-rs", package = "pyth-sdk" }
pyth-sdk-solana = { git = "https://github.com/lbkolev/pyth-sdk-rs", package = "pyth-sdk-solana" }
pyth-sdk = { git = "https://github.com/pyth-network/pyth-sdk-rs", package = "pyth-sdk" }
pyth-sdk-solana = { git = "https://github.com/pyth-network/pyth-sdk-rs", package = "pyth-sdk-solana" }
raydium-clmm = { git = "https://github.com/raydium-io/raydium-clmm", package = "raydium-amm-v3", features = ["no-entrypoint", "client"] }
raydium-cp-swap = { git = "https://github.com/raydium-io/raydium-cp-swap", package = "raydium-cp-swap", features = ["no-entrypoint", "client"] }
reqwest = { version = "0.12.24" }
Expand Down
52 changes: 39 additions & 13 deletions crates/magnus/src/adapters/amms/humidifi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub struct HumidifiCfg {
pub market: Pubkey,
pub base_ta: Pubkey,
pub quote_ta: Pubkey,
pub reserve_mints: [(Pubkey, u8); 2],
pub reserve_mints: [(Pubkey, Option<u8>); 2],
}

impl TryFrom<&serde_json::Value> for HumidifiCfg {
Expand All @@ -58,21 +58,32 @@ impl TryFrom<&serde_json::Value> for HumidifiCfg {
return Err("reserve_mints must have exactly 2 elements".to_string());
}

let mint0 = reserve_mints[0].as_array().ok_or("reserve_mints[0] not a string")?;
let (mint0_addr, mint0_dec) = (mint0[0].as_str().ok_or("reserve_mints[0][0] not a string")?, mint0[1].as_u64().ok_or("reserve_mints[0][1] not a u64")?);
let parse_mint = |val: &serde_json::Value| -> Result<(Pubkey, Option<u8>), String> {
if let Some(arr) = val.as_array() {
if arr.len() < 2 {
return Err("reserve_mint array must have at least 2 elements".to_string());
}
let addr_str = arr[0].as_str().ok_or("reserve_mint address not a string")?;
let addr = Pubkey::from_str(addr_str).map_err(|e| e.to_string())?;
let dec = arr[1].as_u64().ok_or("reserve_mint decimals not a u64")? as u8;
Ok((addr, Some(dec)))
} else if let Some(addr_str) = val.as_str() {
let addr = Pubkey::from_str(addr_str).map_err(|e| e.to_string())?;
Ok((addr, None))
} else {
Err("reserve_mint must be an array [address, decimals] or a string address".to_string())
}
};

let mint1 = reserve_mints[1].as_array().ok_or("reserve_mints[1] not a string")?;
let (mint1_addr, mint1_dec) = (mint1[0].as_str().ok_or("reserve_mints[1][0] not a string")?, mint1[1].as_u64().ok_or("reserve_mints[1][1] not a u64")?);
let mint0 = parse_mint(&reserve_mints[0])?;
let mint1 = parse_mint(&reserve_mints[1])?;

Ok(HumidifiCfg {
pubkey: Pubkey::from_str(pubkey).map_err(|e| e.to_string())?,
market: Pubkey::from_str(market).map_err(|e| e.to_string())?,
base_ta: Pubkey::from_str(base_ta).map_err(|e| e.to_string())?,
quote_ta: Pubkey::from_str(quote_ta).map_err(|e| e.to_string())?,
reserve_mints: [
(Pubkey::from_str(mint0_addr).map_err(|e| e.to_string())?, mint0_dec as u8),
(Pubkey::from_str(mint1_addr).map_err(|e| e.to_string())?, mint1_dec as u8),
],
reserve_mints: [mint0, mint1],
})
}
}
Expand All @@ -81,14 +92,29 @@ impl Adapter for Humidifi {}

impl Humidifi {
pub fn new(cfg: HumidifiCfg, client: &RpcClient) -> eyre::Result<Humidifi> {
let mut chroot = Chroot::new(cfg.reserve_mints);
use solana_sdk::program_pack::Pack;

let mut reserve_mints_with_decimals = [(Pubkey::default(), 0u8); 2];
for (i, &(mint, opt_dec)) in cfg.reserve_mints.iter().enumerate() {
let dec = match opt_dec {
Some(d) => d,
None => {
let account = client.get_account(&mint)?;
let mint_state = spl_token::state::Mint::unpack(&account.data)?;
mint_state.decimals
}
};
Comment on lines +99 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

spl-token 9.0.0 Mint::unpack Token-2022 extensions compatibility

💡 Result:

In SPL Token 9.0.0, the Mint::unpack method remains designed specifically for the original SPL Token program's fixed-size 82-byte data structure [1][2]. It is not compatible with Token-2022 mint accounts that utilize extensions [3]. Key technical distinctions include: 1. Data Layout: The original SPL Token program stores mint data in a fixed 82-byte layout [1][2]. Token-2022 utilizes a Type-Length-Value (TLV) structure to support extensions, meaning mint accounts can have a variable size greater than 82 bytes [3][4]. 2. Unpacking Mechanism: Because Mint::unpack expects exactly 82 bytes, attempting to use it on a Token-2022 mint account (which contains additional TLV data) will fail if the data length validation is strictly enforced, or it will ignore the extension data entirely if only the first 82 bytes are read [4][1]. 3. Recommended Approach: To interact with Token-2022 mints, you must use the spl-token-2022 crate, which provides PodStateWithExtensions or equivalent helpers to correctly parse the base state along with the appended TLV extension data [3][4]. For developers needing a unified approach without deep dependencies, generic tools like spl-generic-token may be used for basic fields (supply, decimals) [5], but they intentionally exclude access to extensions. Any logic requiring extension support (e.g., transfer hooks, metadata, or confidential transfers) must utilize the spl-token-2022 library structures [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant region.
wc -l crates/magnus/src/adapters/amms/humidifi.rs
ast-grep outline crates/magnus/src/adapters/amms/humidifi.rs --view expanded
sed -n '1,180p' crates/magnus/src/adapters/amms/humidifi.rs

# Look for related mint-decimals handling and token-program branching in the repo.
rg -n "Mint::unpack|spl_token::state::Mint|Token-2022|token-2022|owner.*mint|decimals" crates/magnus/src -S

Repository: LimeChain/magnus

Length of output: 12471


Handle Token-2022 mints here too
Mint::unpack only reads the legacy 82-byte mint layout, so any reserve mint with Token-2022 extensions will fail when decimals is omitted. Either require decimals in config for those mints or branch on the mint program and parse Token-2022 state accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/magnus/src/adapters/amms/humidifi.rs` around lines 99 - 106, Update
the opt_dec fallback in the surrounding token-decimal resolution flow to support
both legacy SPL Token and Token-2022 mint accounts. Determine the mint’s owning
program, keep spl_token::state::Mint::unpack for legacy mints, and use the
Token-2022 mint parser for Token-2022 accounts so extensions do not cause
failure; retain the configured decimals path unchanged.

reserve_mints_with_decimals[i] = (mint, dec);
}

let mut chroot = Chroot::new(reserve_mints_with_decimals);
chroot.load_program(ROUTER_ID, "./cfg/programs/magnus-router.so")?;
chroot.load_program(Pubkey::from_str_const(&pmm_humidifi::id().to_string()), "./cfg/programs/humidifi.so")?;

let accs = client.get_multiple_accounts(&[cfg.market, cfg.base_ta, cfg.quote_ta])?;
chroot.load_accounts(vec![(cfg.market, accs[0].clone().unwrap()), (cfg.base_ta, accs[1].clone().unwrap()), (cfg.quote_ta, accs[2].clone().unwrap())])?;
Comment on lines 114 to 115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and inspect the relevant section with line numbers.
git ls-files crates/magnus/src/adapters/amms/humidifi.rs
wc -l crates/magnus/src/adapters/amms/humidifi.rs
sed -n '90,140p' crates/magnus/src/adapters/amms/humidifi.rs

# Find the surrounding types and helper methods involved in this path.
rg -n "get_multiple_accounts|load_accounts|struct .*Config|impl .*humidifi|new\(" crates/magnus/src/adapters/amms/humidifi.rs crates/magnus/src -g '!target'

Repository: LimeChain/magnus

Length of output: 15133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the bootstrap path and account-loading helper to understand how failures surface.
sed -n '1,120p' crates/magnus/src/bootstrap.rs
sed -n '110,150p' crates/magnus/src/adapters/amms.rs

Repository: LimeChain/magnus

Length of output: 4459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm every call site of Humidifi::new and how its Result is handled.
rg -n "Humidifi::new\(" crates/magnus/src
rg -n "unable to initialise humidifi|expect\\(\"unable to initialise humidifi\"\\)" crates/magnus/src

Repository: LimeChain/magnus

Length of output: 426


Replace the unwrap()s with error handling for missing accounts. get_multiple_accounts can return None for any of cfg.market, cfg.base_ta, or cfg.quote_ta; using ok_or_else(...) here would make startup failures point to the missing key instead of panicking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/magnus/src/adapters/amms/humidifi.rs` around lines 114 - 115, Replace
the three unwrap calls in the account-loading flow around get_multiple_accounts
and chroot.load_accounts with ok_or_else-based error handling that returns an
error identifying the specific missing key for cfg.market, cfg.base_ta, or
cfg.quote_ta. Preserve the existing account ordering and propagate the resulting
errors instead of panicking.


cfg.reserve_mints.iter().try_for_each(|(mint_addr, _)| -> eyre::Result<()> {
reserve_mints_with_decimals.iter().try_for_each(|(mint_addr, _)| -> eyre::Result<()> {
let ata = Chroot::mk_ata(mint_addr, &chroot.wallet_pubkey(), 0);
let addr = chroot.wallet_ata(mint_addr);
chroot.load_accounts(vec![(addr, ata)])
Expand Down Expand Up @@ -250,8 +276,8 @@ mod tests {

assert_eq!(cfg.pubkey, Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap());
assert_eq!(cfg.market, Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap());
assert_eq!(cfg.reserve_mints[0].1, 6);
assert_eq!(cfg.reserve_mints[1].1, 9);
assert_eq!(cfg.reserve_mints[0].1, Some(6));
assert_eq!(cfg.reserve_mints[1].1, Some(9));
}

#[test]
Expand Down
3 changes: 2 additions & 1 deletion crates/magnus/src/adapters/amms/obric_v2/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ impl AccountDeserialize for PriceFeed {
let account: &GenericPriceAccount<32, ()> = load_price_account(data).map_err(|_x| error!(ObricError::PythError))?;

// Use a dummy key since the key field will be removed from the SDK
let feed = account.to_price_feed(&PYTH_PROGRAM_ID);
let pyth_pubkey = anchor_lang::prelude::Pubkey::new_from_array(PYTH_PROGRAM_ID.to_bytes());
let feed = account.to_price_feed(&pyth_pubkey);
Ok(PriceFeed(feed))
}
}
Expand Down
18 changes: 10 additions & 8 deletions crates/magnus/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,22 @@ pub fn load(file: &str, client: &RpcClient) -> eyre::Result<Vec<Box<dyn Amm>>> {
let pmms = if let serde_json::Value::Array(items) = &cfgs {
items
.iter()
.map(|item| -> Box<dyn Amm> {
let dex = Dex::from_str(item.get("dex").and_then(|dex| dex.as_str()).expect("no DEX provided")).map_err(|e| eyre!(e)).expect("");
.filter_map(|item| -> Option<Box<dyn Amm>> {
let dex_str = item.get("dex").and_then(|dex| dex.as_str()).expect("no DEX provided");
let dex = Dex::from_str(dex_str).map_err(|e| eyre!(e)).expect("");

let init: Box<dyn Amm> = match dex {
match dex {
Dex::HumidiFi => {
let cfg = HumidifiCfg::try_from(item).map_err(|e| eyre!(e)).expect("");
let amm = Humidifi::new(cfg, client).expect("unable to initialise humidifi");

Box::new(amm)
Some(Box::new(amm) as Box<dyn Amm>)
}
_ => panic!("Unsupported DEX: {}", dex),
};

init
_ => {
tracing::info!("Skipping unsupported/non-bootstrapped DEX in bootstrap load: {}", dex);
None
}
}
Comment on lines +24 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Humidifi::new panic on transient RPC failure undermines the resilience goal.

The PR prevents panics for unsupported DEX types, but line 31's .expect("unable to initialise humidifi") will still panic if Humidifi::new fails. Since new makes live RPC calls (client.get_account, client.get_multiple_accounts), a transient network error or missing account on a single entry will abort the entire bootstrap, skipping all subsequent entries. Consider logging and returning None (or propagating the error) instead of panicking.

🛡️ Proposed fix: skip failed Humidifi entries instead of panicking
                 match dex {
                     Dex::HumidiFi => {
-                        let cfg = HumidifiCfg::try_from(item).map_err(|e| eyre!(e)).expect("");
-                        let amm = Humidifi::new(cfg, client).expect("unable to initialise humidifi");
-
-                        Some(Box::new(amm) as Box<dyn Amm>)
+                        match HumidifiCfg::try_from(item).map_err(|e| eyre!(e)) {
+                            Ok(cfg) => match Humidifi::new(cfg, client) {
+                                Ok(amm) => Some(Box::new(amm) as Box<dyn Amm>),
+                                Err(e) => {
+                                    tracing::warn!("Failed to initialise Humidifi AMM: {}", e);
+                                    None
+                                }
+                            },
+                            Err(e) => {
+                                tracing::warn!("Failed to parse Humidifi config: {}", e);
+                                None
+                            }
+                        }
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.filter_map(|item| -> Option<Box<dyn Amm>> {
let dex_str = item.get("dex").and_then(|dex| dex.as_str()).expect("no DEX provided");
let dex = Dex::from_str(dex_str).map_err(|e| eyre!(e)).expect("");
let init: Box<dyn Amm> = match dex {
match dex {
Dex::HumidiFi => {
let cfg = HumidifiCfg::try_from(item).map_err(|e| eyre!(e)).expect("");
let amm = Humidifi::new(cfg, client).expect("unable to initialise humidifi");
Box::new(amm)
Some(Box::new(amm) as Box<dyn Amm>)
}
_ => panic!("Unsupported DEX: {}", dex),
};
init
_ => {
tracing::info!("Skipping unsupported/non-bootstrapped DEX in bootstrap load: {}", dex);
None
}
}
.filter_map(|item| -> Option<Box<dyn Amm>> {
let dex_str = item.get("dex").and_then(|dex| dex.as_str()).expect("no DEX provided");
let dex = Dex::from_str(dex_str).map_err(|e| eyre!(e)).expect("");
match dex {
Dex::HumidiFi => {
match HumidifiCfg::try_from(item).map_err(|e| eyre!(e)) {
Ok(cfg) => match Humidifi::new(cfg, client) {
Ok(amm) => Some(Box::new(amm) as Box<dyn Amm>),
Err(e) => {
tracing::warn!("Failed to initialise Humidifi AMM: {}", e);
None
}
},
Err(e) => {
tracing::warn!("Failed to parse Humidifi config: {}", e);
None
}
}
}
_ => {
tracing::info!("Skipping unsupported/non-bootstrapped DEX in bootstrap load: {}", dex);
None
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/magnus/src/bootstrap.rs` around lines 24 - 39, Update the Humidifi
branch in the bootstrap loader to handle `Humidifi::new(cfg, client)` failure
without panicking: log the initialization error and return `None` for that entry
so subsequent entries continue processing. Preserve successful initialization by
returning the boxed AMM as before.

})
.collect()
} else {
Expand Down