From f625ee59795813b2567c9eedd1d638bcfdceb437 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 18 Aug 2026 07:17:49 +0200 Subject: [PATCH] v0.2.2-pre.002-fix.001 --- Cargo.toml | 4 +- crates/ksp-onchain-transport-lib/src/lib.rs | 48 ++-- .../src/rpc_accounts.rs | 37 ++- .../src/rpc_cluster.rs | 265 +++++++++++++----- .../src/rpc_common.rs | 28 +- .../src/rpc_tokens.rs | 7 +- .../tests/public_api.rs | 1 - .../unit_tests/rpc_accounts.rs | 57 +++- .../unit_tests/rpc_cluster.rs | 63 ++++- .../unit_tests/rpc_common.rs | 4 +- .../unit_tests/rpc_tokens.rs | 16 +- deltas/0.2.2/pre.002-fix.001.md | 213 ++++++++++++++ 12 files changed, 606 insertions(+), 137 deletions(-) create mode 100644 deltas/0.2.2/pre.002-fix.001.md diff --git a/Cargo.toml b/Cargo.toml index f9e8269..8937212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 111 +# version: 112 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"] [workspace.package] -version = "0.2.2-pre.2" +version = "0.2.2-pre.2.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index 44e0d72..ca57f4b 100644 --- a/crates/ksp-onchain-transport-lib/src/lib.rs +++ b/crates/ksp-onchain-transport-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/lib.rs -// version: 7 +// version: 8 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -91,34 +91,24 @@ pub use self::resilience::HttpRetryCause; pub use self::resilience::HttpRetryDecision; /// Evaluates the centralized bounded HTTP retry policy for one audited RPC method. pub use self::resilience::evaluate_transport_retry; -/// Optional typed configuration for the `getBalance` canary. -pub use self::rpc_canary::GetBalanceConfig; -/// Typed lamport balance returned by the `getBalance` canary. -pub use self::rpc_canary::GetBalanceResult; -/// Typed genesis hash returned by the `getGenesisHash` canary. -pub use self::rpc_canary::SolanaGenesisHash; -/// Typed healthy result returned by the `getHealth` canary. -pub use self::rpc_canary::SolanaNodeHealth; -/// Typed software-version response returned by the `getVersion` canary. -pub use self::rpc_canary::SolanaNodeVersion; -/// Account-data encoding accepted by Solana HTTP account methods. -pub use self::rpc_accounts::SolanaAccountEncoding; /// Typed transport-level Solana account without Program/SPL decoding. pub use self::rpc_accounts::SolanaAccount; /// Address and lamport balance returned by `getLargestAccounts`. pub use self::rpc_accounts::SolanaAccountBalance; /// Wire-preserving account data returned by Solana HTTP account methods. pub use self::rpc_accounts::SolanaAccountData; +/// Account-data encoding accepted by Solana HTTP account methods. +pub use self::rpc_accounts::SolanaAccountEncoding; /// Shared account configuration used by account-info and token-account list methods. pub use self::rpc_accounts::SolanaAccountInfoConfig; /// Byte range requested from account data without decoding it locally. pub use self::rpc_accounts::SolanaDataSliceConfig; /// One public key plus its account returned by account-list RPC methods. pub use self::rpc_accounts::SolanaKeyedAccount; -/// Filter accepted by `getLargestAccounts`. -pub use self::rpc_accounts::SolanaLargestAccountsFilter; /// Optional configuration for `getLargestAccounts`. pub use self::rpc_accounts::SolanaLargestAccountsConfig; +/// Filter accepted by `getLargestAccounts`. +pub use self::rpc_accounts::SolanaLargestAccountsFilter; /// Bytes used by a `memcmp` program-account filter. pub use self::rpc_accounts::SolanaMemcmpBytes; /// One `memcmp` filter applied to account data. @@ -131,6 +121,16 @@ pub use self::rpc_accounts::SolanaProgramAccountFilter; pub use self::rpc_accounts::SolanaProgramAccountsConfig; /// Result union returned by `getProgramAccounts` with or without an RPC context. pub use self::rpc_accounts::SolanaProgramAccountsResult; +/// Optional typed configuration for the `getBalance` canary. +pub use self::rpc_canary::GetBalanceConfig; +/// Typed lamport balance returned by the `getBalance` canary. +pub use self::rpc_canary::GetBalanceResult; +/// Typed genesis hash returned by the `getGenesisHash` canary. +pub use self::rpc_canary::SolanaGenesisHash; +/// Typed healthy result returned by the `getHealth` canary. +pub use self::rpc_canary::SolanaNodeHealth; +/// Typed software-version response returned by the `getVersion` canary. +pub use self::rpc_canary::SolanaNodeVersion; /// Contact information returned for one cluster node. pub use self::rpc_cluster::SolanaClusterNode; /// Epoch-credit history entry returned by `getVoteAccounts`. @@ -163,15 +163,11 @@ pub use self::rpc_common::SolanaContextConfig; pub use self::rpc_common::SolanaRpcContext; /// Generic contextual result returned by typed Solana HTTP RPC adapters. pub use self::rpc_common::SolanaRpcResponse; -/// Exclusive selector accepted by token-account list RPC methods. -pub use self::rpc_tokens::SolanaTokenAccountSelector; -/// Token-account balance entry returned by `getTokenLargestAccounts`. -pub use self::rpc_tokens::SolanaTokenAccountBalance; -/// Token amount returned by Solana HTTP token RPC methods. -pub use self::rpc_tokens::SolanaTokenAmount; -/// Decodes one private serde wire type into the shared Transport error domain. +#[cfg(test)] +/// Decodes one private serde wire type into the shared Transport error domain for staged DTO tests. pub(crate) use self::rpc_common::decode_wire_json; -/// Parses a base58 public key without echoing its wire value into diagnostics. +#[cfg(test)] +/// Parses a base58 public key without echoing its wire value into diagnostics for staged DTO tests. pub(crate) use self::rpc_common::parse_wire_pubkey; /// Functional category used by the audited Solana HTTP JSON-RPC registry. pub use self::rpc_method::HttpRpcCategory; @@ -195,6 +191,12 @@ pub use self::rpc_method::current_http_rpc_methods; pub use self::rpc_method::find_http_rpc_method; /// Returns historically documented deprecated HTTP RPC descriptors retained for compliance history. pub use self::rpc_method::historical_http_rpc_methods; +/// Token-account balance entry returned by `getTokenLargestAccounts`. +pub use self::rpc_tokens::SolanaTokenAccountBalance; +/// Exclusive selector accepted by token-account list RPC methods. +pub use self::rpc_tokens::SolanaTokenAccountSelector; +/// Token amount returned by Solana HTTP token RPC methods. +pub use self::rpc_tokens::SolanaTokenAmount; /// Open cluster or network descriptor used by HTTP endpoint settings. pub use self::settings::HttpClusterName; /// Runtime settings for one role declared by an HTTP endpoint. diff --git a/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs b/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs index 7d00421..d8097cd 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs -// version: 1 +// version: 2 /// Account-data encoding accepted by Solana HTTP account methods. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -29,6 +29,7 @@ impl SolanaAccountEncoding { }; } + #[cfg(test)] fn from_wire(value: &str) -> std::option::Option { return match value { "binary" => std::option::Option::Some(Self::Binary), @@ -67,6 +68,7 @@ impl SolanaDataSliceConfig { return self.length; } + #[cfg(test)] fn to_json_value(self) -> serde_json::Value { return serde_json::json!({"offset": self.offset, "length": self.length}); } @@ -116,14 +118,9 @@ impl SolanaAccountInfoConfig { return self.context.min_context_slot(); } - /// Returns whether this config serializes to an empty JSON object. - #[must_use] - pub(crate) const fn is_empty(&self) -> bool { - return self.encoding.is_none() && self.data_slice.is_none() && self.context.is_empty(); - } - /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] + #[cfg(test)] pub(crate) fn to_json_value(&self) -> serde_json::Value { let context_value = self.context.to_json_value(); let mut object = match context_value { @@ -150,6 +147,7 @@ pub enum SolanaLargestAccountsFilter { } impl SolanaLargestAccountsFilter { + #[cfg(test)] fn as_str(self) -> &'static str { return match self { Self::Circulating => "circulating", @@ -197,6 +195,7 @@ impl SolanaLargestAccountsConfig { /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] + #[cfg(test)] pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { @@ -249,6 +248,7 @@ impl SolanaMemcmpFilter { return &self.bytes; } + #[cfg(test)] fn to_json_value(&self) -> serde_json::Value { return match &self.bytes { crate::SolanaMemcmpBytes::Base58(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base58"}), @@ -270,6 +270,7 @@ pub enum SolanaProgramAccountFilter { } impl SolanaProgramAccountFilter { + #[cfg(test)] fn to_json_value(&self) -> serde_json::Value { return match self { Self::DataSize(size) => serde_json::json!({"dataSize": size}), @@ -326,6 +327,7 @@ impl SolanaProgramAccountsConfig { /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] + #[cfg(test)] pub(crate) fn to_json_value(&self) -> serde_json::Value { let account_value = self.account_config.to_json_value(); let mut object = match account_value { @@ -391,6 +393,7 @@ pub enum SolanaAccountData { } impl SolanaAccountData { + #[cfg(test)] fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -399,11 +402,9 @@ impl SolanaAccountData { }; return match wire { WireAccountData::LegacyBinary(value) => std::result::Result::Ok(Self::LegacyBinary(value)), - WireAccountData::JsonParsed(value) => std::result::Result::Ok(Self::JsonParsed(crate::SolanaParsedAccountData { - program: value.program, - parsed: value.parsed, - space: value.space, - })), + WireAccountData::JsonParsed(value) => { + std::result::Result::Ok(Self::JsonParsed(crate::SolanaParsedAccountData { program: value.program, parsed: value.parsed, space: value.space })) + }, WireAccountData::Encoded((data, encoding)) => { let parsed = crate::SolanaAccountEncoding::from_wire(encoding.as_str()); let encoding = match parsed { @@ -413,7 +414,7 @@ impl SolanaAccountData { ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an unknown encoding") .with_context("rpc_method", method), ); - } + }, }; if encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::JsonParsed { return std::result::Result::Err( @@ -422,7 +423,7 @@ impl SolanaAccountData { ); } std::result::Result::Ok(Self::Encoded { data, encoding }) - } + }, }; } } @@ -476,6 +477,7 @@ impl SolanaAccount { } /// Decodes one account DTO from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -524,6 +526,7 @@ impl SolanaKeyedAccount { } /// Decodes one keyed account from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -565,6 +568,7 @@ impl SolanaAccountBalance { } /// Decodes one account-balance entry from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -588,6 +592,7 @@ pub enum SolanaProgramAccountsResult { Context(crate::SolanaRpcResponse>), } +#[cfg(test)] #[derive(serde::Deserialize)] #[serde(untagged)] enum WireAccountData { @@ -596,6 +601,7 @@ enum WireAccountData { Encoded((std::string::String, std::string::String)), } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireParsedAccountData { program: std::string::String, @@ -603,6 +609,7 @@ struct WireParsedAccountData { space: u64, } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireAccount { lamports: u64, @@ -615,12 +622,14 @@ struct WireAccount { space: std::option::Option, } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireKeyedAccount { pubkey: std::string::String, account: serde_json::Value, } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireAccountBalance { address: std::string::String, diff --git a/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs b/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs index 253ac0d..070b740 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_cluster.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs -// version: 1 +// version: 2 /// Contact information returned for one cluster node. #[derive(Clone, Debug, Eq, PartialEq)] @@ -24,51 +24,82 @@ pub struct SolanaClusterNode { impl SolanaClusterNode { /// Returns the node identity public key. #[must_use] - pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.pubkey; } + pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { + return &self.pubkey; + } /// Returns the optional feature-set identifier. #[must_use] - pub const fn feature_set(&self) -> std::option::Option { return self.feature_set; } + pub const fn feature_set(&self) -> std::option::Option { + return self.feature_set; + } /// Returns the optional gossip endpoint. #[must_use] - pub fn gossip(&self) -> std::option::Option<&str> { return self.gossip.as_deref(); } + pub fn gossip(&self) -> std::option::Option<&str> { + return self.gossip.as_deref(); + } /// Returns the optional PubSub endpoint. #[must_use] - pub fn pubsub(&self) -> std::option::Option<&str> { return self.pubsub.as_deref(); } + pub fn pubsub(&self) -> std::option::Option<&str> { + return self.pubsub.as_deref(); + } /// Returns the optional JSON-RPC endpoint. #[must_use] - pub fn rpc(&self) -> std::option::Option<&str> { return self.rpc.as_deref(); } + pub fn rpc(&self) -> std::option::Option<&str> { + return self.rpc.as_deref(); + } /// Returns the optional repair endpoint. #[must_use] - pub fn serve_repair(&self) -> std::option::Option<&str> { return self.serve_repair.as_deref(); } + pub fn serve_repair(&self) -> std::option::Option<&str> { + return self.serve_repair.as_deref(); + } /// Returns the optional shred version. #[must_use] - pub const fn shred_version(&self) -> std::option::Option { return self.shred_version; } + pub const fn shred_version(&self) -> std::option::Option { + return self.shred_version; + } /// Returns the optional TPU endpoint. #[must_use] - pub fn tpu(&self) -> std::option::Option<&str> { return self.tpu.as_deref(); } + pub fn tpu(&self) -> std::option::Option<&str> { + return self.tpu.as_deref(); + } /// Returns the optional TPU forwards endpoint. #[must_use] - pub fn tpu_forwards(&self) -> std::option::Option<&str> { return self.tpu_forwards.as_deref(); } + pub fn tpu_forwards(&self) -> std::option::Option<&str> { + return self.tpu_forwards.as_deref(); + } /// Returns the optional TPU forwards QUIC endpoint. #[must_use] - pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> { return self.tpu_forwards_quic.as_deref(); } + pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> { + return self.tpu_forwards_quic.as_deref(); + } /// Returns the optional TPU QUIC endpoint. #[must_use] - pub fn tpu_quic(&self) -> std::option::Option<&str> { return self.tpu_quic.as_deref(); } + pub fn tpu_quic(&self) -> std::option::Option<&str> { + return self.tpu_quic.as_deref(); + } /// Returns the optional TPU vote endpoint. #[must_use] - pub fn tpu_vote(&self) -> std::option::Option<&str> { return self.tpu_vote.as_deref(); } + pub fn tpu_vote(&self) -> std::option::Option<&str> { + return self.tpu_vote.as_deref(); + } /// Returns the optional TVU endpoint. #[must_use] - pub fn tvu(&self) -> std::option::Option<&str> { return self.tvu.as_deref(); } + pub fn tvu(&self) -> std::option::Option<&str> { + return self.tvu.as_deref(); + } /// Returns the optional software-version string. #[must_use] - pub fn version(&self) -> std::option::Option<&str> { return self.version.as_deref(); } + pub fn version(&self) -> std::option::Option<&str> { + return self.version.as_deref(); + } /// Returns the optional Agave client identifier extension. #[must_use] - pub fn client_id(&self) -> std::option::Option<&str> { return self.client_id.as_deref(); } + pub fn client_id(&self) -> std::option::Option<&str> { + return self.client_id.as_deref(); + } /// Decodes one cluster-node contact record from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -114,24 +145,37 @@ pub struct SolanaEpochInfo { impl SolanaEpochInfo { /// Returns the absolute slot. #[must_use] - pub const fn absolute_slot(&self) -> u64 { return self.absolute_slot; } + pub const fn absolute_slot(&self) -> u64 { + return self.absolute_slot; + } /// Returns the block height. #[must_use] - pub const fn block_height(&self) -> u64 { return self.block_height; } + pub const fn block_height(&self) -> u64 { + return self.block_height; + } /// Returns the epoch number. #[must_use] - pub const fn epoch(&self) -> u64 { return self.epoch; } + pub const fn epoch(&self) -> u64 { + return self.epoch; + } /// Returns the slot index within the epoch. #[must_use] - pub const fn slot_index(&self) -> u64 { return self.slot_index; } + pub const fn slot_index(&self) -> u64 { + return self.slot_index; + } /// Returns the number of slots in the epoch. #[must_use] - pub const fn slots_in_epoch(&self) -> u64 { return self.slots_in_epoch; } + pub const fn slots_in_epoch(&self) -> u64 { + return self.slots_in_epoch; + } /// Returns the nullable transaction count. #[must_use] - pub const fn transaction_count(&self) -> std::option::Option { return self.transaction_count; } + pub const fn transaction_count(&self) -> std::option::Option { + return self.transaction_count; + } /// Decodes epoch information from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { @@ -161,21 +205,32 @@ pub struct SolanaEpochSchedule { impl SolanaEpochSchedule { /// Returns the first normal epoch. #[must_use] - pub const fn first_normal_epoch(&self) -> u64 { return self.first_normal_epoch; } + pub const fn first_normal_epoch(&self) -> u64 { + return self.first_normal_epoch; + } /// Returns the first normal slot. #[must_use] - pub const fn first_normal_slot(&self) -> u64 { return self.first_normal_slot; } + pub const fn first_normal_slot(&self) -> u64 { + return self.first_normal_slot; + } /// Returns the leader-schedule slot offset. #[must_use] - pub const fn leader_schedule_slot_offset(&self) -> u64 { return self.leader_schedule_slot_offset; } + pub const fn leader_schedule_slot_offset(&self) -> u64 { + return self.leader_schedule_slot_offset; + } /// Returns the number of slots per epoch. #[must_use] - pub const fn slots_per_epoch(&self) -> u64 { return self.slots_per_epoch; } + pub const fn slots_per_epoch(&self) -> u64 { + return self.slots_per_epoch; + } /// Returns whether epoch warmup is enabled. #[must_use] - pub const fn warmup(&self) -> bool { return self.warmup; } + pub const fn warmup(&self) -> bool { + return self.warmup; + } /// Decodes an epoch schedule from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { @@ -201,12 +256,17 @@ pub struct SolanaSnapshotSlotInfo { impl SolanaSnapshotSlotInfo { /// Returns the highest full snapshot slot. #[must_use] - pub const fn full(&self) -> u64 { return self.full; } + pub const fn full(&self) -> u64 { + return self.full; + } /// Returns the optional highest incremental snapshot slot. #[must_use] - pub const fn incremental(&self) -> std::option::Option { return self.incremental; } + pub const fn incremental(&self) -> std::option::Option { + return self.incremental; + } /// Decodes snapshot-slot information from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { @@ -231,11 +291,19 @@ impl SolanaLeaderScheduleConfig { } /// Returns the optional validator identity filter. #[must_use] - pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.identity.as_ref(); } + pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { + return self.identity.as_ref(); + } /// Returns the optional commitment level. #[must_use] - pub const fn commitment(&self) -> std::option::Option { return self.commitment; } - fn is_empty(&self) -> bool { return self.identity.is_none() && self.commitment.is_none(); } + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } + #[cfg(test)] + fn is_empty(&self) -> bool { + return self.identity.is_none() && self.commitment.is_none(); + } + #[cfg(test)] fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(identity) = self.identity.as_ref() { @@ -263,12 +331,15 @@ pub enum SolanaLeaderScheduleRequest { } impl Default for SolanaLeaderScheduleRequest { - fn default() -> Self { return Self::CurrentEpoch(std::option::Option::None); } + fn default() -> Self { + return Self::CurrentEpoch(std::option::Option::None); + } } impl SolanaLeaderScheduleRequest { /// Serializes the typed overload to the exact positional JSON-RPC params. #[must_use] + #[cfg(test)] pub(crate) fn to_json_params(&self) -> std::vec::Vec { return match self { Self::CurrentEpoch(std::option::Option::None) => std::vec::Vec::new(), @@ -290,9 +361,12 @@ pub struct SolanaLeaderSchedule { impl SolanaLeaderSchedule { /// Returns the complete leader schedule map. #[must_use] - pub const fn entries(&self) -> &std::collections::BTreeMap> { return &self.entries; } + pub const fn entries(&self) -> &std::collections::BTreeMap> { + return &self.entries; + } /// Decodes a leader schedule map from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::>>(method, value); let wire = match decoded { @@ -334,18 +408,27 @@ impl SolanaVoteAccountsConfig { } /// Returns the optional commitment. #[must_use] - pub const fn commitment(&self) -> std::option::Option { return self.commitment; } + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } /// Returns the optional vote-account public key filter. #[must_use] - pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.vote_pubkey.as_ref(); } + pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { + return self.vote_pubkey.as_ref(); + } /// Returns whether unstaked delinquent validators should be kept. #[must_use] - pub const fn keep_unstaked_delinquents(&self) -> std::option::Option { return self.keep_unstaked_delinquents; } + pub const fn keep_unstaked_delinquents(&self) -> std::option::Option { + return self.keep_unstaked_delinquents; + } /// Returns the optional delinquent slot distance. #[must_use] - pub const fn delinquent_slot_distance(&self) -> std::option::Option { return self.delinquent_slot_distance; } + pub const fn delinquent_slot_distance(&self) -> std::option::Option { + return self.delinquent_slot_distance; + } /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] + #[cfg(test)] pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { @@ -375,13 +458,19 @@ pub struct SolanaEpochCredits { impl SolanaEpochCredits { /// Returns the epoch number. #[must_use] - pub const fn epoch(&self) -> u64 { return self.epoch; } + pub const fn epoch(&self) -> u64 { + return self.epoch; + } /// Returns cumulative credits at the end of the epoch. #[must_use] - pub const fn credits(&self) -> u64 { return self.credits; } + pub const fn credits(&self) -> u64 { + return self.credits; + } /// Returns cumulative credits before the epoch. #[must_use] - pub const fn previous_credits(&self) -> u64 { return self.previous_credits; } + pub const fn previous_credits(&self) -> u64 { + return self.previous_credits; + } } /// One validator vote-account record returned by `getVoteAccounts`. @@ -401,33 +490,52 @@ pub struct SolanaVoteAccountInfo { impl SolanaVoteAccountInfo { /// Returns the vote account public key. #[must_use] - pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.vote_pubkey; } + pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey { + return &self.vote_pubkey; + } /// Returns the validator identity public key. #[must_use] - pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.node_pubkey; } + pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey { + return &self.node_pubkey; + } /// Returns the activated stake in lamports. #[must_use] - pub const fn activated_stake(&self) -> u64 { return self.activated_stake; } + pub const fn activated_stake(&self) -> u64 { + return self.activated_stake; + } /// Returns the legacy/effective percentage commission field. #[must_use] - pub const fn commission(&self) -> u8 { return self.commission; } + pub const fn commission(&self) -> u8 { + return self.commission; + } /// Returns the optional raw inflation-rewards commission in basis points. #[must_use] - pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option { return self.inflation_rewards_commission_bps; } + pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option { + return self.inflation_rewards_commission_bps; + } /// Returns whether the vote account is staked for the current epoch. #[must_use] - pub const fn epoch_vote_account(&self) -> bool { return self.epoch_vote_account; } + pub const fn epoch_vote_account(&self) -> bool { + return self.epoch_vote_account; + } /// Returns the bounded RPC epoch-credit history. #[must_use] - pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] { return self.epoch_credits.as_slice(); } + pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] { + return self.epoch_credits.as_slice(); + } /// Returns the latest voted slot or zero when no vote exists. #[must_use] - pub const fn last_vote(&self) -> u64 { return self.last_vote; } + pub const fn last_vote(&self) -> u64 { + return self.last_vote; + } /// Returns the current root slot or zero when no root exists. #[must_use] - pub const fn root_slot(&self) -> u64 { return self.root_slot; } + pub const fn root_slot(&self) -> u64 { + return self.root_slot; + } /// Decodes one vote-account record from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -472,12 +580,17 @@ pub struct SolanaVoteAccountStatus { impl SolanaVoteAccountStatus { /// Returns current vote accounts. #[must_use] - pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] { return self.current.as_slice(); } + pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] { + return self.current.as_slice(); + } /// Returns delinquent vote accounts. #[must_use] - pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] { return self.delinquent.as_slice(); } + pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] { + return self.delinquent.as_slice(); + } /// Decodes the complete vote-account status response from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -504,26 +617,42 @@ impl SolanaVoteAccountStatus { } } +#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireClusterNode { pubkey: std::string::String, - #[serde(default)] feature_set: std::option::Option, - #[serde(default)] gossip: std::option::Option, - #[serde(default)] pubsub: std::option::Option, - #[serde(default)] rpc: std::option::Option, - #[serde(default)] serve_repair: std::option::Option, - #[serde(default)] shred_version: std::option::Option, - #[serde(default)] tpu: std::option::Option, - #[serde(default)] tpu_forwards: std::option::Option, - #[serde(default)] tpu_forwards_quic: std::option::Option, - #[serde(default)] tpu_quic: std::option::Option, - #[serde(default)] tpu_vote: std::option::Option, - #[serde(default)] tvu: std::option::Option, - #[serde(default)] version: std::option::Option, - #[serde(default)] client_id: std::option::Option, + #[serde(default)] + feature_set: std::option::Option, + #[serde(default)] + gossip: std::option::Option, + #[serde(default)] + pubsub: std::option::Option, + #[serde(default)] + rpc: std::option::Option, + #[serde(default)] + serve_repair: std::option::Option, + #[serde(default)] + shred_version: std::option::Option, + #[serde(default)] + tpu: std::option::Option, + #[serde(default)] + tpu_forwards: std::option::Option, + #[serde(default)] + tpu_forwards_quic: std::option::Option, + #[serde(default)] + tpu_quic: std::option::Option, + #[serde(default)] + tpu_vote: std::option::Option, + #[serde(default)] + tvu: std::option::Option, + #[serde(default)] + version: std::option::Option, + #[serde(default)] + client_id: std::option::Option, } +#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireEpochInfo { @@ -535,6 +664,7 @@ struct WireEpochInfo { transaction_count: std::option::Option, } +#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireEpochSchedule { @@ -545,12 +675,14 @@ struct WireEpochSchedule { warmup: bool, } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireSnapshotSlotInfo { full: u64, incremental: std::option::Option, } +#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireVoteAccountInfo { @@ -566,6 +698,7 @@ struct WireVoteAccountInfo { root_slot: u64, } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireVoteAccountStatus { current: std::vec::Vec, diff --git a/crates/ksp-onchain-transport-lib/src/rpc_common.rs b/crates/ksp-onchain-transport-lib/src/rpc_common.rs index 6bfc146..fe531fa 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_common.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_common.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_common.rs -// version: 1 +// version: 2 /// Commitment level accepted by typed Solana HTTP RPC adapters. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -43,15 +43,10 @@ impl SolanaCommitmentConfig { return self.commitment; } - /// Returns whether this config serializes to an empty JSON object. - #[must_use] - pub(crate) const fn is_empty(&self) -> bool { - return self.commitment.is_none(); - } - /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] - pub(crate) fn to_json_value(&self) -> serde_json::Value { + #[cfg(test)] + pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); @@ -86,15 +81,9 @@ impl SolanaContextConfig { return self.min_context_slot; } - /// Returns whether this config serializes to an empty JSON object. - #[must_use] - pub(crate) const fn is_empty(&self) -> bool { - return self.commitment.is_none() && self.min_context_slot.is_none(); - } - /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] - pub(crate) fn to_json_value(&self) -> serde_json::Value { + pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); @@ -129,7 +118,8 @@ impl SolanaRpcContext { }; } - /// Decodes one RPC context from a parsed JSON value. + /// Decodes one RPC context from a parsed JSON value for staged DTO tests. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let context = match decoded { @@ -160,7 +150,8 @@ impl SolanaRpcResponse { return &self.value; } - /// Creates a contextual response after wire decoding and validation. + /// Creates a contextual response after wire decoding and validation for staged DTO tests. + #[cfg(test)] #[must_use] pub(crate) const fn new(context: crate::SolanaRpcContext, value: T) -> Self { return Self { context, value }; @@ -168,6 +159,7 @@ impl SolanaRpcResponse { } /// Decodes one private serde wire type and maps shape failures to the shared Transport error domain. +#[cfg(test)] pub(crate) fn decode_wire_json(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = serde_json::from_value::(value); return match decoded { @@ -181,6 +173,7 @@ pub(crate) fn decode_wire_json(method: &str, val } /// Parses a base58 public key from one wire field without echoing its value into diagnostics. +#[cfg(test)] pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_core_lib::Result { let parsed = value.parse::(); return match parsed { @@ -193,6 +186,7 @@ pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_c }; } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireRpcContext { slot: u64, diff --git a/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs b/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs index ff980cc..4b23f66 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_tokens.rs -// version: 1 +// version: 2 /// Exclusive selector accepted by token-account list RPC methods. #[derive(Clone, Debug, Eq, PartialEq)] @@ -13,6 +13,7 @@ pub enum SolanaTokenAccountSelector { impl SolanaTokenAccountSelector { /// Serializes the exclusive selector to the Solana JSON-RPC wire object. #[must_use] + #[cfg(test)] pub(crate) fn to_json_value(&self) -> serde_json::Value { return match self { Self::Mint(pubkey) => serde_json::json!({"mint": pubkey.to_string()}), @@ -56,6 +57,7 @@ impl SolanaTokenAmount { } /// Decodes one token amount from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { @@ -91,6 +93,7 @@ impl SolanaTokenAccountBalance { } /// Decodes one token-account balance entry from the Solana JSON wire shape. + #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -112,6 +115,7 @@ impl SolanaTokenAccountBalance { } } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireTokenAmount { amount: std::string::String, @@ -122,6 +126,7 @@ struct WireTokenAmount { ui_amount_string: std::string::String, } +#[cfg(test)] #[derive(serde::Deserialize)] struct WireTokenAccountBalance { address: std::string::String, diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index bd11fa6..01ee836 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -172,7 +172,6 @@ fn public_typed_canary_contracts_are_available_from_crate_root() { assert_eq!(ksp_onchain_transport_lib::SolanaNodeHealth::Healthy, ksp_onchain_transport_lib::SolanaNodeHealth::Healthy); } - #[test] fn public_pre_002_shared_rpc_types_are_constructible_from_crate_root() { let context = ksp_onchain_transport_lib::SolanaContextConfig::new( diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs index c36083a..9404d22 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs -// version: 1 +// version: 2 #[test] fn account_config_serializes_all_common_fields() { @@ -22,7 +22,12 @@ fn program_accounts_config_preserves_filter_variants_and_flags() { crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(4, crate::SolanaMemcmpBytes::Base64("AQID".to_owned()))), crate::SolanaProgramAccountFilter::TokenAccountState, ]; - let config = crate::SolanaProgramAccountsConfig::new(crate::SolanaAccountInfoConfig::default(), filters, std::option::Option::Some(true), std::option::Option::Some(true)); + let config = crate::SolanaProgramAccountsConfig::new( + crate::SolanaAccountInfoConfig::default(), + filters, + std::option::Option::Some(true), + std::option::Option::Some(true), + ); assert_eq!( config.to_json_value(), serde_json::json!({ @@ -35,19 +40,51 @@ fn program_accounts_config_preserves_filter_variants_and_flags() { #[test] fn account_wire_fixture_preserves_legacy_encoded_and_json_parsed_data() { - let values: std::vec::Vec = serde_json::from_str(include_str!("../fixtures/http/account_data.variants.json")).expect("fixture must decode"); + let values: std::vec::Vec = + serde_json::from_str(include_str!("../fixtures/http/account_data.variants.json")).expect("fixture must decode"); let legacy = crate::SolanaAccount::decode_wire("fixture", values[0].clone()).expect("legacy account must decode"); assert!(matches!(legacy.data(), crate::SolanaAccountData::LegacyBinary(_))); assert_eq!(legacy.space(), std::option::Option::None); let encoded = crate::SolanaAccount::decode_wire("fixture", values[1].clone()).expect("encoded account must decode"); assert!(matches!(encoded.data(), crate::SolanaAccountData::Encoded { encoding: crate::SolanaAccountEncoding::Base64Zstd, .. })); let parsed = crate::SolanaAccount::decode_wire("fixture", values[2].clone()).expect("parsed account must decode"); - match parsed.data() { - crate::SolanaAccountData::JsonParsed(value) => { - assert_eq!(value.program(), "spl-token"); - assert_eq!(value.space(), 165); - assert_eq!(value.parsed()["type"], serde_json::json!("account")); - } - _ => assert!(false, "jsonParsed fixture must retain parsed data"), + assert!(matches!(parsed.data(), crate::SolanaAccountData::JsonParsed(_)), "jsonParsed fixture must retain parsed data"); + if let crate::SolanaAccountData::JsonParsed(value) = parsed.data() { + assert_eq!(value.program(), "spl-token"); + assert_eq!(value.space(), 165); + assert_eq!(value.parsed()["type"], serde_json::json!("account")); } } + +#[test] +fn staged_largest_and_keyed_account_helpers_match_wire_shapes() { + let config = crate::SolanaLargestAccountsConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(crate::SolanaLargestAccountsFilter::NonCirculating), + std::option::Option::Some(true), + ); + assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"finalized","filter":"nonCirculating","sortResults":true})); + + let keyed = crate::SolanaKeyedAccount::decode_wire( + "fixture", + serde_json::json!({ + "pubkey":"11111111111111111111111111111111", + "account":{ + "lamports":42, + "data":["", "base64"], + "owner":"11111111111111111111111111111111", + "executable":false, + "rentEpoch":0, + "space":0 + } + }), + ) + .expect("keyed account must decode"); + assert_eq!(keyed.pubkey().to_string(), "11111111111111111111111111111111"); + assert_eq!(keyed.account().lamports(), 42); + + let balance = crate::SolanaAccountBalance::decode_wire("fixture", serde_json::json!({"address":"11111111111111111111111111111111","lamports":99})) + .expect("account balance must decode"); + assert_eq!(balance.address().to_string(), "11111111111111111111111111111111"); + assert_eq!(balance.lamports(), 99); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs index 091d388..fd304ca 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs -// version: 1 +// version: 2 #[test] fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() { @@ -50,3 +50,64 @@ fn leader_schedule_request_encodes_current_epoch_and_slot_overloads_without_ambi std::vec![serde_json::json!(123), serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})] ); } + +#[test] +fn staged_epoch_snapshot_and_leader_helpers_preserve_wire_shapes() { + let epoch = crate::SolanaEpochInfo::decode_wire( + "getEpochInfo", + serde_json::json!({"absoluteSlot":10,"blockHeight":9,"epoch":2,"slotIndex":3,"slotsInEpoch":32,"transactionCount":null}), + ) + .expect("epoch info must decode"); + assert_eq!(epoch.transaction_count(), std::option::Option::None); + + let schedule = crate::SolanaEpochSchedule::decode_wire( + "getEpochSchedule", + serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}), + ) + .expect("epoch schedule must decode"); + assert_eq!(schedule.slots_per_epoch(), 64); + + let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null})) + .expect("snapshot info must decode"); + assert_eq!(snapshot.full(), 100); + assert_eq!(snapshot.incremental(), std::option::Option::None); + + let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]})) + .expect("leader schedule must decode"); + assert_eq!(leader.entries().len(), 1); +} + +#[test] +fn staged_vote_status_and_config_helpers_preserve_wire_shapes() { + let vote_pubkey = "11111111111111111111111111111111".parse::().expect("fixture pubkey must parse"); + let config = crate::SolanaVoteAccountsConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(vote_pubkey), + std::option::Option::Some(true), + std::option::Option::Some(128), + ); + assert_eq!( + config.to_json_value(), + serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128}) + ); + + let status = crate::SolanaVoteAccountStatus::decode_wire( + "getVoteAccounts", + serde_json::json!({ + "current":[{ + "votePubkey":"11111111111111111111111111111111", + "nodePubkey":"11111111111111111111111111111111", + "activatedStake":1, + "commission":5, + "epochVoteAccount":true, + "epochCredits":[], + "lastVote":2, + "rootSlot":1 + }], + "delinquent":[] + }), + ) + .expect("vote account status must decode"); + assert_eq!(status.current().len(), 1); + assert!(status.delinquent().is_empty()); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs index a585153..1b3386b 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs -// version: 1 +// version: 2 #[test] fn shared_context_configs_preserve_commitment_and_min_context_slot() { @@ -14,6 +14,8 @@ fn rpc_context_preserves_nullable_api_version() { let with_version = crate::SolanaRpcContext::decode_wire("fixture", serde_json::json!({"slot":10,"apiVersion":"4.2.1"})).expect("context must decode"); assert_eq!(with_version.slot(), 10); assert_eq!(with_version.api_version(), std::option::Option::Some("4.2.1")); + let response = crate::SolanaRpcResponse::new(with_version, 7_u64); + assert_eq!(response.value(), &7_u64); let without_version = crate::SolanaRpcContext::decode_wire("fixture", serde_json::json!({"slot":11})).expect("context must decode"); assert_eq!(without_version.api_version(), std::option::Option::None); } diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs index 8e4790a..c9fb237 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs -// version: 1 +// version: 2 #[test] fn token_selector_is_exclusive_by_construction() { @@ -17,3 +17,17 @@ fn token_amount_fixture_preserves_nullable_ui_amount() { assert_eq!(amount.ui_amount(), std::option::Option::None); assert_eq!(amount.ui_amount_string(), "18446744073.709551615"); } + +#[test] +fn staged_token_account_balance_helper_preserves_address_and_amount() { + let value = serde_json::json!({ + "address":"11111111111111111111111111111111", + "amount":"10", + "decimals":2, + "uiAmount":0.1, + "uiAmountString":"0.1" + }); + let balance = crate::SolanaTokenAccountBalance::decode_wire("fixture", value).expect("token account balance must decode"); + assert_eq!(balance.address().to_string(), "11111111111111111111111111111111"); + assert_eq!(balance.amount().amount(), "10"); +} diff --git a/deltas/0.2.2/pre.002-fix.001.md b/deltas/0.2.2/pre.002-fix.001.md new file mode 100644 index 0000000..e7076d8 --- /dev/null +++ b/deltas/0.2.2/pre.002-fix.001.md @@ -0,0 +1,213 @@ + + + +# Delta `0.2.2-pre.002-fix.001` — nettoyage des helpers RPC préparatoires et Clippy + +## Base requise + +```text +livraison : 0.2.2-pre.002 +workspace.package.version = "0.2.2-pre.2" +``` + +Le correctif est ouvert après la validation locale communiquée le 2026-08-18. Les tests de `pre.002` passent, mais `cargo check` et Clippy +révèlent que plusieurs helpers privés de sérialisation/décodage ont été compilés trop tôt alors qu'aucun wrapper `0.2.2` ne les consomme encore. + +## Objectif + +Corriger exclusivement les warnings de qualité de `pre.002`, sans commencer les wrappers Accounts de `pre.003`, sans modifier les DTOs publics +et sans masquer les lints avec `#[allow(dead_code)]`. + +Les catégories observées sont : + +```text +dead_code helpers/wire structs préparatoires Accounts/Tokens/Cluster/Common +clippy::assertions_on_constants assert!(false, ...) dans un test Account +clippy::wrong_self_convention 2 serializers de configs Copy prenant &self +``` + +## Version Cargo + +Le correctif modifie du Rust de production/test. Conformément à `VER-ID-007` et `VER-ID-010` : + +```text +0.2.2-pre.2 -> 0.2.2-pre.2.fix.1 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou retirée. + +## Correction du staging privé + +`pre.002` doit établir les DTOs/configs publics et les fixtures communes, mais les wrappers qui consommeront réellement les helpers privés sont +répartis sur `pre.003` à `pre.006`. + +Les serializers/décodeurs privés et leurs structs wire encore utilisés uniquement par les tests sont donc compilés sous : + +```rust +#[cfg(test)] +``` + +jusqu'à la prerelease qui les active réellement dans un wrapper de production. + +Cette correction : + +- conserve tous les types, constructeurs, getters, enums et réexports publics introduits par `pre.002` ; +- conserve les fixtures Account/Token/Cluster/Vote ; +- ne rend aucun helper privé artificiellement public ; +- n'ajoute aucun `#[allow(dead_code)]` ; +- évite de porter du code privé mort dans la bibliothèque normale entre deux tranches ; +- impose aux prereleases suivantes de retirer `#[cfg(test)]` uniquement sur les helpers qu'elles commencent effectivement à consommer. + +Les deux helpers `is_empty()` qui n'ont aucun consommateur ni test utile à ce stade sont supprimés et seront réintroduits seulement si un wrapper +les nécessite réellement. + +## Correction Clippy + +### Configs `Copy` + +Les serializers internes de : + +```text +SolanaCommitmentConfig +SolanaContextConfig +``` + +prennent désormais `self` par valeur, ce qui correspond à leur nature `Copy` et supprime `clippy::wrong_self_convention` sans modifier la forme JSON. + +`SolanaContextConfig::to_json_value()` reste production-live parce que le canari `getBalance` de `0.2.1` le consomme déjà. + +### Assertion constante + +Le test `account_wire_fixture_preserves_legacy_encoded_and_json_parsed_data` remplace le `match` avec `assert!(false, ...)` par : + +1. une assertion `matches!` sur `JsonParsed` ; +2. un `if let` pour vérifier le contenu. + +Aucun `panic!()` ou `unreachable!()` explicite n'est introduit. + +## Complément de tests préparatoires + +Des tests unitaires sont ajoutés pour exercer les helpers privés désormais explicitement test-only qui n'étaient pas couverts par `pre.002` : + +- sérialisation `SolanaLargestAccountsConfig` ; +- décodage `SolanaKeyedAccount` et `SolanaAccountBalance` ; +- décodage `SolanaTokenAccountBalance` ; +- décodage `SolanaEpochInfo`, `SolanaEpochSchedule`, `SolanaSnapshotSlotInfo` et `SolanaLeaderSchedule` ; +- sérialisation `SolanaVoteAccountsConfig` ; +- décodage `SolanaVoteAccountStatus` ; +- construction test-only de `SolanaRpcResponse`. + +Les fixtures et invariants `clientId`, `inflationRewardsCommissionBps`, `uiAmount: null`, `space: null` et Account data restent couverts. + +## Fichiers ajoutés + +```text +deltas/0.2.2/pre.002-fix.001.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_common.rs +crates/ksp-onchain-transport-lib/src/rpc_accounts.rs +crates/ksp-onchain-transport-lib/src/rpc_tokens.rs +crates/ksp-onchain-transport-lib/src/rpc_cluster.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs +``` + +## Fichiers supprimés + +Aucun. + +## Fichiers volontairement inchangés + +```text +deltas/0.2.2/pre.002.md +docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md +ROADMAP.md +CHANGELOG.md +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +crates/ksp-onchain-transport-lib/src/rpc_method.rs +crates/ksp-config-lib/** +config/** +``` + +Le delta `pre.002.md` reste une trace historique et n'est pas réécrit. + +## Validation de `pre.002` ayant déclenché le correctif + +Les commandes suivantes ont été exécutées par l'utilisateur sur `0.2.2-pre.002` avant création de ce fix : + +```text +cargo fmt --all : exécuté +cargo check --workspace : OK, avec 42 warnings dead_code Transport +cargo clippy --workspace --all-targets : OK, avec les warnings dead_code + assertions_on_constants + wrong_self_convention +cargo test -p ksp-onchain-transport-lib : OK +``` + +Résultats de tests communiqués : + +```text +81 tests unitaires : passés +9 tests public_api : passés +2 tests release_completeness : passés +0 échec +``` + +Ces résultats prouvent la fonctionnalité de la base `pre.002`, mais ne constituent pas une validation du présent fix. + +## Contrôles statiques effectués pendant la préparation du fix + +- aucune occurrence `#[allow(dead_code)]` ajoutée ; +- aucune occurrence `assert!(false` restante dans les fichiers modifiés ; +- les deux serializers de configs `Copy` prennent `self` par valeur ; +- les helpers privés préparatoires non consommés par la bibliothèque normale sont explicitement `#[cfg(test)]` ; +- les réexports crate-private de support wire sont eux aussi test-only tant qu'aucun wrapper ne les consomme ; +- les helpers test-only identifiés disposent d'un consommateur dans les tests unitaires ; +- aucune méthode RPC `0.2.2` n'est ajoutée à la surface typed-complete ; +- aucune dépendance/feature/configuration Transport n'est modifiée ; +- headers/version des fichiers modifiés incrémentés ; +- `pre.002.md` et le plan `009` restent inchangés. + +## Validations du correctif non exécutées dans le sandbox + +Le sandbox de préparation ne fournit pas `cargo`, `rustc` ni `rustfmt`. Les validations Rust du fix ne sont donc pas déclarées réussies. + +Après application, exécuter au minimum : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Conformément aux règles Rust KSP, `cargo test --workspace` reste requis au checkpoint de clôture approprié. Aucun `cargo tree` supplémentaire n'est +requis pour ce fix puisqu'aucune dépendance ni feature n'a changé. + +Résultat attendu immédiatement pour les commandes ci-dessus : aucune régression des canaris `0.2.1`, aucun warning ayant motivé ce fix et quatre +tests unitaires Transport supplémentaires par rapport à `pre.002`. + +## Décisions prises + +- corriger la cause des warnings plutôt que les masquer avec un lint allowance ; +- conserver les DTOs et fixtures publics de `pre.002` ; +- considérer les serializers/décodeurs privés non encore consommés comme support de test jusqu'à leur activation réelle ; +- ne pas rendre `serde_json::Value` public uniquement pour éviter `dead_code` ; +- ne pas commencer les cinq wrappers Accounts dans ce fix ; +- conserver `pre.003` comme prochaine tranche fonctionnelle après validation du correctif. + +## Questions ouvertes + +Aucune question bloquante. + +## Suite + +Après validation et commit de `0.2.2-pre.002-fix.001`, poursuivre avec `0.2.2-pre.003` pour les cinq wrappers Accounts. Les helpers Account +nécessaires seront alors sortis de `#[cfg(test)]` au fur et à mesure de leur consommation réelle.