From b723208c2ddf771503e954aa0a993902c5aad198 Mon Sep 17 00:00:00 2001 From: Prasanna Gautam Date: Fri, 8 May 2026 16:58:16 +0000 Subject: [PATCH 1/2] chore(solana): forbid HashMap/HashSet via clippy disallowed-types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `src/chain_parsers/visualsign-solana/clippy.toml` enforcing `disallowed-types` for `std::collections::HashMap` and `std::collections::HashSet`, and migrates all internal usage in the visualsign-solana crate to BTreeMap/ BTreeSet. Rationale: 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` per preset) must be stable across runs. HashMap's randomized hasher silently breaks this — and we just shipped a fix for that exact bug in squads_multisig. Forbidding HashMap crate-wide prevents the pattern from being reintroduced. Boundary conversions (3 sites) collect into the inferred upstream type at the call site so we keep BTreeMap internally and only convert at FFI points (solana_parser::parse_transaction, SolanaParsedInstructionData.named_accounts, generated SolanaMetadata.idl_mappings). Verified: cargo clippy --all-targets -- -D warnings clean across the workspace, `make -C src test` passes (127 unit + 49 integration tests). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../visualsign-solana/clippy.toml | 22 +++++++++++++++++ .../visualsign-solana/src/core/mod.rs | 4 ++-- .../visualsign-solana/src/core/txtypes/v0.rs | 11 +++++---- .../visualsign-solana/src/core/visualsign.rs | 6 ++--- .../visualsign-solana/src/idl/mod.rs | 24 +++++++++---------- .../associated_token_account/config.rs | 4 ++-- .../src/presets/compute_budget/config.rs | 4 ++-- .../src/presets/dflow_aggregator/config.rs | 6 ++--- .../src/presets/jupiter_borrow/config.rs | 6 ++--- .../src/presets/jupiter_earn/config.rs | 6 ++--- .../src/presets/jupiter_perps/config.rs | 6 ++--- .../src/presets/jupiter_swap/config.rs | 6 ++--- .../src/presets/kamino_borrow/config.rs | 6 ++--- .../src/presets/kamino_farms/config.rs | 6 ++--- .../src/presets/kamino_vault/config.rs | 6 ++--- .../metadao_conditional_vault/config.rs | 6 ++--- .../src/presets/metadao_futarchy/config.rs | 6 ++--- .../src/presets/meteora_damm_v2/config.rs | 6 ++--- .../src/presets/meteora_damm_v2/mod.rs | 2 +- .../src/presets/meteora_dlmm/config.rs | 6 ++--- .../src/presets/neutral_trade/config.rs | 6 ++--- .../src/presets/onre_app/config.rs | 8 +++---- .../src/presets/orca_whirlpool/config.rs | 6 ++--- .../src/presets/stakepool/config.rs | 6 ++--- .../src/presets/swig_wallet/config.rs | 8 +++---- .../src/presets/system/config.rs | 6 ++--- .../src/presets/token_2022/config.rs | 6 ++--- .../src/presets/unknown_program/config.rs | 4 ++-- .../src/presets/unknown_program/mod.rs | 10 +++++--- .../visualsign-solana/src/utils/mod.rs | 6 ++--- .../visualsign-solana/tests/common/mod.rs | 8 ++++--- .../tests/real_idl_validation.rs | 4 ++-- 32 files changed, 129 insertions(+), 98 deletions(-) create mode 100644 src/chain_parsers/visualsign-solana/clippy.toml 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/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..399993933 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,7 +57,7 @@ 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 + // (`parsed.named_accounts` from solana_parser is `BTreeMap`, 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..9562e468a 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, @@ -315,7 +315,7 @@ fn try_parse_with_idl( 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 +333,11 @@ fn try_parse_with_idl( } } - parsed.named_accounts = named_accounts; + // Boundary conversion: `SolanaParsedInstructionData.named_accounts` is the upstream + // solana_parser HashMap type; collecting into the inferred field type lets us + // build the local map as a BTreeMap (per crate-wide determinism rule in clippy.toml) + // and convert only at the assignment boundary. + parsed.named_accounts = named_accounts.into_iter().collect(); Ok(parsed) } 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()), From de639a37c76abddc4c7c5e30018a85ca3f7309b0 Mon Sep 17 00:00:00 2001 From: Prasanna Gautam Date: Mon, 11 May 2026 21:35:38 +0000 Subject: [PATCH 2/2] address review: fix unknown_program determinism, comment typo, post-rebase test fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - unknown_program: refactor try_parse_with_idl to return (parsed, BTreeMap) so the render path iterates the local BTreeMap instead of the upstream HashMap-backed parsed.named_accounts (was a real determinism bug — fields rendered in HashMap iteration order). - meteora_damm_v2: correct in-source comment that mis-named the upstream type as BTreeMap; it is a HashMap. - dflow_aggregator: post-rebase test fix — the new test from #286 used std::collections::HashMap::new() which trips the new disallowed-types lint; switch to Default::default() (inferred HashMap, no source mention). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/presets/dflow_aggregator/mod.rs | 2 +- .../src/presets/meteora_damm_v2/mod.rs | 4 +-- .../src/presets/unknown_program/mod.rs | 32 +++++++++++-------- 3 files changed, 21 insertions(+), 17 deletions(-) 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/meteora_damm_v2/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/meteora_damm_v2/mod.rs index 399993933..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 `BTreeMap`, 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/unknown_program/mod.rs b/src/chain_parsers/visualsign-solana/src/presets/unknown_program/mod.rs index 9562e468a..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 @@ -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,7 +321,7 @@ 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 @@ -333,11 +343,5 @@ fn try_parse_with_idl( } } - // Boundary conversion: `SolanaParsedInstructionData.named_accounts` is the upstream - // solana_parser HashMap type; collecting into the inferred field type lets us - // build the local map as a BTreeMap (per crate-wide determinism rule in clippy.toml) - // and convert only at the assignment boundary. - parsed.named_accounts = named_accounts.into_iter().collect(); - - Ok(parsed) + Ok((parsed, named_accounts)) }