diff --git a/src/chain_parsers/visualsign-solana/clippy.toml b/src/chain_parsers/visualsign-solana/clippy.toml new file mode 100644 index 000000000..8711ba967 --- /dev/null +++ b/src/chain_parsers/visualsign-solana/clippy.toml @@ -0,0 +1,22 @@ +# Determinism enforcement for visualsign-solana. +# +# The rendered SignablePayload JSON is consumed downstream for hashing and +# wallet display, so iteration order over any map that ends up in the output +# (e.g. `named_accounts` in each preset's `mod.rs`) MUST be stable across +# runs. HashMap/HashSet's randomized hasher silently breaks this. +# +# Use BTreeMap/BTreeSet instead. The performance delta is negligible at the +# sizes we use these maps (typically <100 entries) and the determinism +# guarantee is worth the trade. +# +# This rule is intentionally crate-wide rather than scoped to `presets/` — +# even maps that look "lookup-only" (e.g. config.rs routing tables, the IDL +# registry's internal maps) can leak iteration order via debug formatting, +# error messages, or future code paths that walk them. Forcing BTreeMap +# everywhere keeps the determinism story simple and review-friendly: no +# `#[allow(clippy::disallowed_types)]` escape hatches in the codebase. + +disallowed-types = [ + { path = "std::collections::HashMap", reason = "use BTreeMap — iteration order affects rendered SignablePayload output and breaks deterministic hashing. See clippy.toml comment." }, + { path = "std::collections::HashSet", reason = "use BTreeSet for the same reason as HashMap." }, +] diff --git a/src/chain_parsers/visualsign-solana/src/core/mod.rs b/src/chain_parsers/visualsign-solana/src/core/mod.rs index ee81fc370..b7614c6b2 100644 --- a/src/chain_parsers/visualsign-solana/src/core/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/core/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use ::visualsign::AnnotatedPayloadField; use ::visualsign::errors::VisualSignError; @@ -88,7 +88,7 @@ impl<'a> VisualizerContext<'a> { } pub struct SolanaIntegrationConfigData { - pub programs: HashMap<&'static str, HashMap<&'static str, Vec<&'static str>>>, + pub programs: BTreeMap<&'static str, BTreeMap<&'static str, Vec<&'static str>>>, } pub trait SolanaIntegrationConfig { fn new() -> Self diff --git a/src/chain_parsers/visualsign-solana/src/core/txtypes/v0.rs b/src/chain_parsers/visualsign-solana/src/core/txtypes/v0.rs index 48a9049ce..e1ca4ca7d 100644 --- a/src/chain_parsers/visualsign-solana/src/core/txtypes/v0.rs +++ b/src/chain_parsers/visualsign-solana/src/core/txtypes/v0.rs @@ -17,7 +17,7 @@ pub fn decode_v0_transfers( ) -> Result, VisualSignError> { use crate::presets::jupiter_swap::{JUPITER_IDL_JSON, JUPITER_PROGRAM_ID}; use solana_parser::solana::parser::parse_transaction; - use std::collections::HashMap; + use std::collections::BTreeMap; // Serialize the full versioned transaction let transaction_bytes = bincode::serialize(versioned_tx).map_err(|e| { @@ -29,18 +29,21 @@ pub fn decode_v0_transfers( // Override the stale Jupiter v6 IDL bundled in solana_parser with our locally // refreshed copy so that newer instructions (e.g. route_v2) don't cause the // whole-tx decode to bail with "no matching instruction discriminator". - let mut custom_idls: HashMap = HashMap::new(); + let mut custom_idls: BTreeMap = BTreeMap::new(); custom_idls.insert( JUPITER_PROGRAM_ID.to_string(), (JUPITER_IDL_JSON.to_string(), true), ); let is_full_transaction = true; // true because we're passing full tx and not message - // Parse using solana-parser which handles V0 transactions and lookup tables + // Parse using solana-parser which handles V0 transactions and lookup tables. + // Boundary conversion: solana_parser's `parse_transaction` API takes a HashMap; + // we collect into the inferred parameter type so our local code keeps working + // with BTreeMap (per crate-wide determinism rule in clippy.toml). let parsed_transaction = parse_transaction( hex::encode(transaction_bytes), is_full_transaction, - Some(custom_idls), + Some(custom_idls.into_iter().collect()), ) .map_err(|e| { VisualSignError::ParseError(visualsign::vsptrait::TransactionParseError::DecodeError( diff --git a/src/chain_parsers/visualsign-solana/src/core/visualsign.rs b/src/chain_parsers/visualsign-solana/src/core/visualsign.rs index 342ce27e6..9caa15049 100644 --- a/src/chain_parsers/visualsign-solana/src/core/visualsign.rs +++ b/src/chain_parsers/visualsign-solana/src/core/visualsign.rs @@ -10,7 +10,7 @@ use solana_sdk::{ message::VersionedMessage, transaction::{Transaction as SolanaTransaction, VersionedTransaction}, }; -use std::collections::HashMap; +use std::collections::BTreeMap; use visualsign::{ SignablePayload, SignablePayloadField, SignablePayloadFieldCommon, encodings::SupportedEncodings, @@ -89,8 +89,8 @@ impl SolanaTransactionWrapper { /// Extract IDL mappings from VisualSignOptions metadata /// -/// Returns a HashMap of program_id (base58 string) -> (IDL JSON string, program name) -fn extract_idl_mappings(options: &VisualSignOptions) -> HashMap { +/// Returns a BTreeMap of program_id (base58 string) -> (IDL JSON string, program name) +fn extract_idl_mappings(options: &VisualSignOptions) -> BTreeMap { options .metadata .as_ref() diff --git a/src/chain_parsers/visualsign-solana/src/idl/mod.rs b/src/chain_parsers/visualsign-solana/src/idl/mod.rs index 848c1dc30..4e4ba0435 100644 --- a/src/chain_parsers/visualsign-solana/src/idl/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/idl/mod.rs @@ -5,7 +5,7 @@ use solana_parser::{CustomIdl, CustomIdlConfig, Idl, ProgramType, decode_idl_data}; use solana_sdk::pubkey::Pubkey; -use std::collections::HashMap; +use std::collections::BTreeMap; /// Registry for managing program IDLs (program_id -> CustomIdlConfig) /// @@ -19,20 +19,20 @@ use std::collections::HashMap; pub struct IdlRegistry { /// Maps program_id (base58 string) -> CustomIdlConfig /// These are user-provided IDLs that override built-ins - configs: HashMap, + configs: BTreeMap, /// Maps program_id -> human-readable name (extracted from IDL or provided by user) - names: HashMap, + names: BTreeMap, /// Maps program_id -> IDL name from metadata.name in JSON - idl_names: HashMap, + idl_names: BTreeMap, } impl IdlRegistry { /// Create empty registry (built-in IDLs handled by solana_parser directly) pub fn new() -> Self { Self { - configs: HashMap::new(), - names: HashMap::new(), - idl_names: HashMap::new(), + configs: BTreeMap::new(), + names: BTreeMap::new(), + idl_names: BTreeMap::new(), } } @@ -45,11 +45,11 @@ impl IdlRegistry { /// * `Ok(IdlRegistry)` with the custom IDLs configured to override built-ins /// * `Err` if any IDL JSON is invalid pub fn from_idl_mappings( - idl_mappings: HashMap, + idl_mappings: BTreeMap, ) -> Result> { - let mut configs = HashMap::new(); - let mut names = HashMap::new(); - let mut idl_names = HashMap::new(); + let mut configs = BTreeMap::new(); + let mut names = BTreeMap::new(); + let mut idl_names = BTreeMap::new(); for (program_id, (idl_json, program_name)) in idl_mappings { // Extract IDL name from JSON metadata @@ -82,7 +82,7 @@ impl IdlRegistry { /// /// Reserved for future integration with solana_parser's batch transaction parsing. #[allow(dead_code)] - pub fn get_all_configs(&self) -> &HashMap { + pub fn get_all_configs(&self) -> &BTreeMap { &self.configs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/associated_token_account/config.rs b/src/chain_parsers/visualsign-solana/src/presets/associated_token_account/config.rs index 92d618598..9e246b441 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/associated_token_account/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/associated_token_account/config.rs @@ -10,8 +10,8 @@ impl SolanaIntegrationConfig for AssociatedTokenAccountConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = std::collections::HashMap::new(); - let mut ata_instructions = std::collections::HashMap::new(); + let mut programs = std::collections::BTreeMap::new(); + let mut ata_instructions = std::collections::BTreeMap::new(); ata_instructions.insert("*", vec!["*"]); programs.insert( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", diff --git a/src/chain_parsers/visualsign-solana/src/presets/compute_budget/config.rs b/src/chain_parsers/visualsign-solana/src/presets/compute_budget/config.rs index 533ee032a..01988ef2b 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/compute_budget/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/compute_budget/config.rs @@ -10,8 +10,8 @@ impl SolanaIntegrationConfig for ComputeBudgetConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = std::collections::HashMap::new(); - let mut compute_budget_instructions = std::collections::HashMap::new(); + let mut programs = std::collections::BTreeMap::new(); + let mut compute_budget_instructions = std::collections::BTreeMap::new(); compute_budget_instructions.insert("*", vec!["*"]); programs.insert( "ComputeBudget111111111111111111111111111111", diff --git a/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/config.rs b/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/config.rs index 25b07a216..2f61c2b29 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/config.rs @@ -1,6 +1,6 @@ use super::DFLOW_AGGREGATOR_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct DflowAggregatorConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for DflowAggregatorConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(DFLOW_AGGREGATOR_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/mod.rs index 16e6e12f4..65d65eca8 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/dflow_aggregator/mod.rs @@ -445,7 +445,7 @@ mod tests { parsed: SolanaParsedInstructionData { instruction_name: "test_ix".to_string(), discriminator: "00".to_string(), - named_accounts: std::collections::HashMap::new(), + named_accounts: Default::default(), program_call_args: serde_json::Map::new(), idl_source: IdlSource::Custom, idl_hash: String::new(), diff --git a/src/chain_parsers/visualsign-solana/src/presets/jupiter_borrow/config.rs b/src/chain_parsers/visualsign-solana/src/presets/jupiter_borrow/config.rs index aedc6ba21..83d57aaf9 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/jupiter_borrow/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/jupiter_borrow/config.rs @@ -1,6 +1,6 @@ use super::JUPITER_BORROW_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct JupiterBorrowConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for JupiterBorrowConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(JUPITER_BORROW_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/jupiter_earn/config.rs b/src/chain_parsers/visualsign-solana/src/presets/jupiter_earn/config.rs index d3b0ad651..b2028a78d 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/jupiter_earn/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/jupiter_earn/config.rs @@ -1,6 +1,6 @@ use super::JUPITER_EARN_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct JupiterEarnConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for JupiterEarnConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(JUPITER_EARN_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/jupiter_perps/config.rs b/src/chain_parsers/visualsign-solana/src/presets/jupiter_perps/config.rs index 1576a9a7c..d90ef978b 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/jupiter_perps/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/jupiter_perps/config.rs @@ -1,6 +1,6 @@ use super::JUPITER_PERPS_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct JupiterPerpsConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for JupiterPerpsConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(JUPITER_PERPS_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/jupiter_swap/config.rs b/src/chain_parsers/visualsign-solana/src/presets/jupiter_swap/config.rs index d6bcbf51e..256cca4b7 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/jupiter_swap/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/jupiter_swap/config.rs @@ -1,6 +1,6 @@ use super::JUPITER_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct JupiterSwapConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for JupiterSwapConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut jupiter_instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut jupiter_instructions = BTreeMap::new(); jupiter_instructions.insert("*", vec!["*"]); programs.insert(JUPITER_PROGRAM_ID, jupiter_instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/config.rs b/src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/config.rs index 8dcdf6a14..f3b3ce3d9 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/kamino_borrow/config.rs @@ -1,6 +1,6 @@ use super::KAMINO_BORROW_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct KaminoBorrowConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for KaminoBorrowConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(KAMINO_BORROW_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/kamino_farms/config.rs b/src/chain_parsers/visualsign-solana/src/presets/kamino_farms/config.rs index 5d09e0473..6ecc0c0aa 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/kamino_farms/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/kamino_farms/config.rs @@ -1,6 +1,6 @@ use super::KAMINO_FARMS_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct KaminoFarmsConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for KaminoFarmsConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(KAMINO_FARMS_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/kamino_vault/config.rs b/src/chain_parsers/visualsign-solana/src/presets/kamino_vault/config.rs index 998169054..a77ac9009 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/kamino_vault/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/kamino_vault/config.rs @@ -1,6 +1,6 @@ use super::KAMINO_VAULT_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct KaminoVaultConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for KaminoVaultConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(KAMINO_VAULT_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/metadao_conditional_vault/config.rs b/src/chain_parsers/visualsign-solana/src/presets/metadao_conditional_vault/config.rs index 2df0b6af3..581fb2f20 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/metadao_conditional_vault/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/metadao_conditional_vault/config.rs @@ -1,6 +1,6 @@ use super::METADAO_CONDITIONAL_VAULT_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct MetadaoConditionalVaultConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for MetadaoConditionalVaultConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(METADAO_CONDITIONAL_VAULT_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/metadao_futarchy/config.rs b/src/chain_parsers/visualsign-solana/src/presets/metadao_futarchy/config.rs index d28d642fd..53e71928e 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/metadao_futarchy/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/metadao_futarchy/config.rs @@ -1,6 +1,6 @@ use super::METADAO_FUTARCHY_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct MetadaoFutarchyConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for MetadaoFutarchyConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(METADAO_FUTARCHY_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/config.rs b/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/config.rs index bc43ab412..19495e0c5 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/config.rs @@ -1,6 +1,6 @@ use super::METEORA_DAMM_V2_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct MeteoraDammV2Config; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for MeteoraDammV2Config { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(METEORA_DAMM_V2_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/mod.rs index d5551dfab..068457a7d 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/mod.rs @@ -57,8 +57,8 @@ impl InstructionVisualizer for MeteoraDammV2Visualizer { create_text_field("Discriminator", &parsed.discriminator)?, ]; // Iterate the local BTreeMap so the rendered field order is deterministic. - // (`parsed.named_accounts` from solana_parser is `HashMap`, so iteration there - // is non-deterministic.) + // (`parsed.named_accounts` from solana_parser is a `HashMap`, so iteration + // there is non-deterministic.) for (account_name, account_address) in &named_accounts { expanded_fields.push(create_text_field(account_name, account_address)?); } diff --git a/src/chain_parsers/visualsign-solana/src/presets/meteora_dlmm/config.rs b/src/chain_parsers/visualsign-solana/src/presets/meteora_dlmm/config.rs index 8a134c595..c14175fba 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/meteora_dlmm/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/meteora_dlmm/config.rs @@ -1,6 +1,6 @@ use super::METEORA_DLMM_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct MeteoraDlmmConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for MeteoraDlmmConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(METEORA_DLMM_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/neutral_trade/config.rs b/src/chain_parsers/visualsign-solana/src/presets/neutral_trade/config.rs index a386329e9..a588869bf 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/neutral_trade/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/neutral_trade/config.rs @@ -1,6 +1,6 @@ use super::NEUTRAL_TRADE_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct NeutralTradeConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for NeutralTradeConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(NEUTRAL_TRADE_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/onre_app/config.rs b/src/chain_parsers/visualsign-solana/src/presets/onre_app/config.rs index 32cf7beeb..0d5956fc2 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/onre_app/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/onre_app/config.rs @@ -1,6 +1,6 @@ use super::ONRE_APP_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct OnreAppConfig; @@ -12,9 +12,9 @@ impl SolanaIntegrationConfig for OnreAppConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs: HashMap<&'static str, HashMap<&'static str, Vec<&'static str>>> = - HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs: BTreeMap<&'static str, BTreeMap<&'static str, Vec<&'static str>>> = + BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(ONRE_APP_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/orca_whirlpool/config.rs b/src/chain_parsers/visualsign-solana/src/presets/orca_whirlpool/config.rs index 2f14d0464..28a837783 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/orca_whirlpool/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/orca_whirlpool/config.rs @@ -1,6 +1,6 @@ use super::ORCA_WHIRLPOOL_PROGRAM_ID; use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct OrcaWhirlpoolConfig; @@ -12,8 +12,8 @@ impl SolanaIntegrationConfig for OrcaWhirlpoolConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert(ORCA_WHIRLPOOL_PROGRAM_ID, instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/stakepool/config.rs b/src/chain_parsers/visualsign-solana/src/presets/stakepool/config.rs index dda9a1353..715c22f86 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/stakepool/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/stakepool/config.rs @@ -1,7 +1,7 @@ //! Configuration for Stakepool program integration use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct StakepoolConfig; @@ -13,8 +13,8 @@ impl SolanaIntegrationConfig for StakepoolConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut stakepool_instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut stakepool_instructions = BTreeMap::new(); stakepool_instructions.insert("*", vec!["*"]); // this is a weaker version, we can probably do a prefix match on SPoo1 programs.insert( diff --git a/src/chain_parsers/visualsign-solana/src/presets/swig_wallet/config.rs b/src/chain_parsers/visualsign-solana/src/presets/swig_wallet/config.rs index 3faa8f7a5..0c6d9b604 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/swig_wallet/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/swig_wallet/config.rs @@ -8,13 +8,13 @@ impl SolanaIntegrationConfig for SwigWalletConfig { } fn data(&self) -> &SolanaIntegrationConfigData { - use std::collections::HashMap; + use std::collections::BTreeMap; static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs: HashMap<&'static str, HashMap<&'static str, Vec<&'static str>>> = - HashMap::new(); - let mut instructions = HashMap::new(); + let mut programs: BTreeMap<&'static str, BTreeMap<&'static str, Vec<&'static str>>> = + BTreeMap::new(); + let mut instructions = BTreeMap::new(); instructions.insert("*", vec!["*"]); programs.insert("swigypWHEksbC64pWKwah1WTeh9JXwx8H1rJHLdbQMB", instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/system/config.rs b/src/chain_parsers/visualsign-solana/src/presets/system/config.rs index 8da9766c6..776e3e269 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/system/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/system/config.rs @@ -1,7 +1,7 @@ //! Configuration for System program integration use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct SystemConfig; @@ -13,8 +13,8 @@ impl SolanaIntegrationConfig for SystemConfig { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut system_instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut system_instructions = BTreeMap::new(); system_instructions.insert("*", vec!["*"]); programs.insert("11111111111111111111111111111111", system_instructions); SolanaIntegrationConfigData { programs } diff --git a/src/chain_parsers/visualsign-solana/src/presets/token_2022/config.rs b/src/chain_parsers/visualsign-solana/src/presets/token_2022/config.rs index 076f25856..3c84437ca 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/token_2022/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/token_2022/config.rs @@ -1,7 +1,7 @@ //! Configuration for Token 2022 program integration use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct Token2022Config; @@ -13,8 +13,8 @@ impl SolanaIntegrationConfig for Token2022Config { fn data(&self) -> &SolanaIntegrationConfigData { static DATA: std::sync::OnceLock = std::sync::OnceLock::new(); DATA.get_or_init(|| { - let mut programs = HashMap::new(); - let mut token2022_instructions = HashMap::new(); + let mut programs = BTreeMap::new(); + let mut token2022_instructions = BTreeMap::new(); token2022_instructions.insert("*", vec!["*"]); // Token 2022 program ID programs.insert( diff --git a/src/chain_parsers/visualsign-solana/src/presets/unknown_program/config.rs b/src/chain_parsers/visualsign-solana/src/presets/unknown_program/config.rs index 0d0132b5e..327176d32 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/unknown_program/config.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/unknown_program/config.rs @@ -2,7 +2,7 @@ //! This is a catch-all visualizer that handles any program not supported by other visualizers use crate::core::{SolanaIntegrationConfig, SolanaIntegrationConfigData}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub struct UnknownProgramConfig; @@ -16,7 +16,7 @@ impl SolanaIntegrationConfig for UnknownProgramConfig { DATA.get_or_init(|| { // This is a catch-all - it doesn't match specific programs // Instead, can_handle is overridden to always return true - let programs = HashMap::new(); + let programs = BTreeMap::new(); SolanaIntegrationConfigData { programs } }) } diff --git a/src/chain_parsers/visualsign-solana/src/presets/unknown_program/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/unknown_program/mod.rs index 4e0a563ae..7270cac14 100644 --- a/src/chain_parsers/visualsign-solana/src/presets/unknown_program/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/presets/unknown_program/mod.rs @@ -8,7 +8,7 @@ use crate::core::{ }; use config::UnknownProgramConfig; use solana_parser::{SolanaParsedInstructionData, parse_instruction_with_idl}; -use std::collections::HashMap; +use std::collections::BTreeMap; use visualsign::errors::VisualSignError; use visualsign::{ AnnotatedPayloadField, SignablePayloadField, SignablePayloadFieldCommon, @@ -64,7 +64,10 @@ fn try_idl_parsing( let program_name = idl_registry.get_program_name(program_id); let idl_name = idl_registry.get_idl_name(program_id); - // Try to parse the instruction with IDL + // Try to parse the instruction with IDL. + // `parsed_result` carries the upstream parsed payload alongside a locally-built + // `BTreeMap` of named accounts so iteration order at render time is deterministic + // (the upstream `SolanaParsedInstructionData.named_accounts` is a `HashMap`). let parsed_result = try_parse_with_idl(instruction, idl_registry); let instruction_data_hex = hex::encode(&instruction.data); @@ -120,7 +123,7 @@ fn try_idl_parsing( // Add parsed instruction fields if IDL parsing succeeded match parsed_result { - Ok(parsed) => { + Ok((parsed, named_accounts)) => { // Add instruction name to condensed view condensed_fields.push(AnnotatedPayloadField { signable_payload_field: SignablePayloadField::TextV2 { @@ -166,8 +169,10 @@ fn try_idl_parsing( dynamic_annotation: None, }); - // Add named accounts (e.g., mint, depositor_token_account, etc.) - for (account_name, account_address) in &parsed.named_accounts { + // Add named accounts (e.g., mint, depositor_token_account, etc.). + // Iterate the local BTreeMap rather than `parsed.named_accounts`, which is + // the upstream `HashMap` and would render fields in non-deterministic order. + for (account_name, account_address) in &named_accounts { expanded_fields.push(AnnotatedPayloadField { signable_payload_field: SignablePayloadField::TextV2 { common: SignablePayloadFieldCommon { @@ -297,11 +302,16 @@ fn create_unknown_program_preview_layout( }) } -/// Try to parse instruction using the new parse_instruction_with_idl function +/// Try to parse instruction using the new parse_instruction_with_idl function. +/// +/// Returns both the upstream parsed payload and a locally-built `BTreeMap` of named +/// accounts. We deliberately do NOT write the named accounts back into +/// `parsed.named_accounts` (a `HashMap` at the FFI boundary): the rendering path needs +/// stable iteration order, so callers should iterate the returned `BTreeMap` instead. fn try_parse_with_idl( instruction: &solana_sdk::instruction::Instruction, idl_registry: &crate::idl::IdlRegistry, -) -> Result> { +) -> Result<(SolanaParsedInstructionData, BTreeMap), Box> { let program_id_str = instruction.program_id.to_string(); let instruction_data = &instruction.data; @@ -311,11 +321,11 @@ fn try_parse_with_idl( .ok_or("No IDL found for program")?; // Parse the instruction with the IDL - let mut parsed: SolanaParsedInstructionData = + let parsed: SolanaParsedInstructionData = parse_instruction_with_idl(instruction_data, &program_id_str, &idl)?; // Manually create the named_accounts map by matching instruction accounts with IDL - let mut named_accounts = HashMap::new(); + let mut named_accounts = BTreeMap::new(); // Find the matching instruction in the IDL to get account names if let Some(idl_instruction) = idl.instructions.iter().find(|inst| { @@ -333,7 +343,5 @@ fn try_parse_with_idl( } } - parsed.named_accounts = named_accounts; - - Ok(parsed) + Ok((parsed, named_accounts)) } diff --git a/src/chain_parsers/visualsign-solana/src/utils/mod.rs b/src/chain_parsers/visualsign-solana/src/utils/mod.rs index 44582615b..e5bd3dc8f 100644 --- a/src/chain_parsers/visualsign-solana/src/utils/mod.rs +++ b/src/chain_parsers/visualsign-solana/src/utils/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; // Constants const ADDRESS_TRUNCATION_LENGTH: usize = 8; @@ -33,8 +33,8 @@ pub struct TokenInfo { } /// Static lookup table for common Solana token addresses -pub fn get_token_lookup_table() -> HashMap<&'static str, TokenInfo> { - let mut tokens = HashMap::new(); +pub fn get_token_lookup_table() -> BTreeMap<&'static str, TokenInfo> { + let mut tokens = BTreeMap::new(); // SOL (native) tokens.insert( diff --git a/src/chain_parsers/visualsign-solana/tests/common/mod.rs b/src/chain_parsers/visualsign-solana/tests/common/mod.rs index 2e05b7815..81c55c9b1 100644 --- a/src/chain_parsers/visualsign-solana/tests/common/mod.rs +++ b/src/chain_parsers/visualsign-solana/tests/common/mod.rs @@ -2,7 +2,7 @@ //! Shared test helpers for IDL-based fuzz and integration tests. #![allow(dead_code)] -use std::collections::HashMap; +use std::collections::BTreeMap; use generated::parser::{ChainMetadata, Idl as ProtoIdl, SolanaMetadata, chain_metadata}; use solana_parser::decode_idl_data; @@ -84,7 +84,7 @@ pub fn build_multi_instruction_transaction(pairs: Vec<(Pubkey, Vec)>) -> Sol // ── VisualSignOptions builders ──────────────────────────────────────────────── pub fn options_with_idl(program_id: &Pubkey, idl_json: &str, name: &str) -> VisualSignOptions { - let mut idl_mappings = HashMap::new(); + let mut idl_mappings = BTreeMap::new(); idl_mappings.insert( program_id.to_string(), ProtoIdl { @@ -98,7 +98,9 @@ pub fn options_with_idl(program_id: &Pubkey, idl_json: &str, name: &str) -> Visu VisualSignOptions { metadata: Some(ChainMetadata { metadata: Some(chain_metadata::Metadata::Solana(SolanaMetadata { - idl_mappings, + // Boundary conversion: generated proto type uses HashMap; we keep + // BTreeMap locally per crate-wide determinism rule. + idl_mappings: idl_mappings.into_iter().collect(), network_id: None, idl: None, })), diff --git a/src/chain_parsers/visualsign-solana/tests/real_idl_validation.rs b/src/chain_parsers/visualsign-solana/tests/real_idl_validation.rs index cf53451c4..2d779163f 100644 --- a/src/chain_parsers/visualsign-solana/tests/real_idl_validation.rs +++ b/src/chain_parsers/visualsign-solana/tests/real_idl_validation.rs @@ -42,7 +42,7 @@ fn real_idl_discriminators_are_unique() { let Some((_, idl)) = load_idl_from_env() else { return; }; - let mut seen: std::collections::HashMap, &str> = std::collections::HashMap::new(); + let mut seen: std::collections::BTreeMap, &str> = std::collections::BTreeMap::new(); for inst in &idl.instructions { if let Some(disc) = &inst.discriminator { if let Some(existing) = seen.get(disc) { @@ -63,7 +63,7 @@ fn real_idl_instruction_names_are_unique() { let Some((_, idl)) = load_idl_from_env() else { return; }; - let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new(); + let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new(); for inst in &idl.instructions { assert!( seen.insert(inst.name.as_str()),