diff --git a/Cargo.toml b/Cargo.toml index d48f70e..efb7e41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ 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.4-pre.1" +version = "0.2.4-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json b/crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json new file mode 100644 index 0000000..1ff0a05 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json @@ -0,0 +1,4 @@ +[ + {"commitment":[1,2,3,4],"totalStake":1000000000}, + {"commitment":null,"totalStake":1000000000} +] diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json b/crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json new file mode 100644 index 0000000..e1f0302 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json @@ -0,0 +1,7 @@ +{ + "byIdentity": { + "11111111111111111111111111111111": [8,7], + "Vote111111111111111111111111111111111111111": [4,4] + }, + "range": {"firstSlot":430000000,"lastSlot":430000099} +} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json b/crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json new file mode 100644 index 0000000..15aa10b --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json @@ -0,0 +1,20 @@ +[ + { + "previousBlockhash":"previous-a", + "blockhash":"block-a", + "parentSlot":1, + "blockTime":null, + "blockHeight":null + }, + { + "previousBlockhash":"previous-b", + "blockhash":"block-b", + "parentSlot":2, + "transactions":null, + "signatures":null, + "rewards":null, + "numRewardPartitions":null, + "blockTime":null, + "blockHeight":null + } +] diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json b/crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json new file mode 100644 index 0000000..aa9b6bc --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json @@ -0,0 +1,36 @@ +{ + "previousBlockhash":"previous-blockhash-fixture", + "blockhash":"blockhash-fixture", + "parentSlot":430000122, + "transactions":[ + { + "transaction":{"signatures":["signature-fixture"],"message":{"accountKeys":[],"recentBlockhash":"recent-fixture","instructions":[]}}, + "meta":{"err":null,"fee":5000}, + "version":1 + }, + { + "transaction":["AQID","base64"], + "meta":null + } + ], + "rewards":[ + { + "pubkey":"11111111111111111111111111111111", + "lamports":2500, + "postBalance":1000002500, + "rewardType":"Fee", + "commission":null, + "commissionBps":1234 + }, + { + "pubkey":"Vote111111111111111111111111111111111111111", + "lamports":1250, + "postBalance":2000001250, + "rewardType":"Voting", + "commission":5 + } + ], + "numRewardPartitions":4, + "blockTime":1787072400, + "blockHeight":410000000 +} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json b/crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json new file mode 100644 index 0000000..2682cf3 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json @@ -0,0 +1 @@ +{"initial":0.08,"terminal":0.015,"taper":0.15,"foundation":0.05,"foundationTerm":7.0} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json b/crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json new file mode 100644 index 0000000..1d1db99 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json @@ -0,0 +1 @@ +{"total":0.042,"validator":0.041,"foundation":0.001,"epoch":812} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json b/crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json new file mode 100644 index 0000000..e616c83 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json @@ -0,0 +1,5 @@ +[ + {"epoch":811,"effectiveSlot":429900000,"amount":2500000,"postBalance":1002500000,"commission":7,"commissionBps":725}, + null, + {"epoch":811,"effectiveSlot":429900000,"amount":1250000,"postBalance":501250000,"commission":null} +] diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json b/crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json new file mode 100644 index 0000000..5f19a24 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json @@ -0,0 +1,4 @@ +[ + {"slot":430000123,"numTransactions":250000,"numNonVoteTransactions":175000,"numSlots":120,"samplePeriodSecs":60}, + {"slot":430000003,"numTransactions":240000,"numSlots":118,"samplePeriodSecs":60} +] diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json b/crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json new file mode 100644 index 0000000..26aeee5 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json @@ -0,0 +1,9 @@ +{ + "total":600000000000000000, + "circulating":500000000000000000, + "nonCirculating":100000000000000000, + "nonCirculatingAccounts":[ + "11111111111111111111111111111111", + "Vote111111111111111111111111111111111111111" + ] +} diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index 59b4555..d14bc70 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: 16 +// version: 17 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -12,7 +12,8 @@ //! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC //! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through //! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete -//! modern/legacy `getTransaction` coverage. The `0.2.4` family remains staged. +//! modern/legacy `getTransaction` coverage. `0.2.4-pre.002` adds the shared Blocks/Economics wire, config and result primitives without yet advancing +//! any `V0_2_4` method to a typed wrapper. mod client; mod constants; @@ -22,9 +23,11 @@ mod json_rpc; mod pool; mod resilience; mod rpc_accounts; +mod rpc_blocks; mod rpc_canary; mod rpc_cluster; mod rpc_common; +mod rpc_economics; mod rpc_method; mod rpc_tokens; mod rpc_transactions; @@ -126,6 +129,28 @@ 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; +/// Commitment distribution returned by `getBlockCommitment`. +pub use self::rpc_blocks::SolanaBlockCommitment; +/// Block-production counts returned by `getBlockProduction` before the shared RPC context wrapper is applied. +pub use self::rpc_blocks::SolanaBlockProduction; +/// Optional configuration accepted by `getBlockProduction`. +pub use self::rpc_blocks::SolanaBlockProductionConfig; +/// Slot range accepted inside `getBlockProduction` configuration. +pub use self::rpc_blocks::SolanaBlockProductionRange; +/// Effective range reported inside `getBlockProduction` results. +pub use self::rpc_blocks::SolanaBlockProductionResultRange; +/// One reward entry returned in a confirmed block. +pub use self::rpc_blocks::SolanaBlockReward; +/// One transaction entry returned inside a confirmed block. +pub use self::rpc_blocks::SolanaBlockTransaction; +/// Confirmed block wire result returned by `getBlock` when the RPC result is non-null. +pub use self::rpc_blocks::SolanaConfirmedBlock; +/// Modern configuration object accepted by `getBlock`. +pub use self::rpc_blocks::SolanaGetBlockConfig; +/// One entry returned by `getRecentPerformanceSamples`. +pub use self::rpc_blocks::SolanaPerformanceSample; +/// Transaction detail level accepted by modern `getBlock` requests. +pub use self::rpc_blocks::SolanaTransactionDetails; /// Optional typed configuration for the `getBalance` canary. pub use self::rpc_canary::GetBalanceConfig; /// Typed lamport balance returned by the `getBalance` canary. @@ -172,6 +197,18 @@ pub use self::rpc_common::SolanaRpcResponse; pub(crate) use self::rpc_common::decode_wire_json; /// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters. pub(crate) use self::rpc_common::parse_wire_pubkey; +/// Inflation-governor values returned by `getInflationGovernor`. +pub use self::rpc_economics::SolanaInflationGovernor; +/// Current inflation-rate values returned by `getInflationRate`. +pub use self::rpc_economics::SolanaInflationRate; +/// One non-null positional reward returned by `getInflationReward`. +pub use self::rpc_economics::SolanaInflationReward; +/// Optional epoch/context configuration accepted by `getInflationReward`. +pub use self::rpc_economics::SolanaInflationRewardConfig; +/// Supply totals returned inside the contextual `getSupply` response. +pub use self::rpc_economics::SolanaSupply; +/// Optional configuration accepted by `getSupply`. +pub use self::rpc_economics::SolanaSupplyConfig; /// Functional category used by the audited Solana HTTP JSON-RPC registry. pub use self::rpc_method::HttpRpcCategory; /// Release that owns typed KSP coverage for one audited HTTP RPC method. diff --git a/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs b/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs new file mode 100644 index 0000000..d1b1217 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/rpc_blocks.rs @@ -0,0 +1,712 @@ +// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs +// version: 1 + +/// Transaction detail level accepted by modern `getBlock` requests. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub enum SolanaTransactionDetails { + /// Return complete transaction payloads and metadata. + #[default] + Full, + /// Return transaction signatures only. + Signatures, + /// Return no transaction payloads. + None, + /// Return the account-list transaction representation. + Accounts, +} + +impl SolanaTransactionDetails { + /// Returns the Solana JSON-RPC wire string. + #[must_use] + pub const fn as_str(self) -> &'static str { + return match self { + Self::Full => "full", + Self::Signatures => "signatures", + Self::None => "none", + Self::Accounts => "accounts", + }; + } +} + +/// Modern configuration object accepted by `getBlock`. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct SolanaGetBlockConfig { + commitment: std::option::Option, + encoding: std::option::Option, + transaction_details: std::option::Option, + max_supported_transaction_version: std::option::Option, + rewards: std::option::Option, +} + +impl SolanaGetBlockConfig { + /// Creates a modern `getBlock` configuration. + #[must_use] + pub const fn new( + commitment: std::option::Option, + encoding: std::option::Option, + transaction_details: std::option::Option, + max_supported_transaction_version: std::option::Option, + rewards: std::option::Option, + ) -> Self { + return Self { commitment, encoding, transaction_details, max_supported_transaction_version, rewards }; + } + + /// Returns the optional commitment level. + #[must_use] + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } + + /// Returns the optional transaction encoding. + #[must_use] + pub const fn encoding(&self) -> std::option::Option { + return self.encoding; + } + + /// Returns the optional transaction detail level. + #[must_use] + pub const fn transaction_details(&self) -> std::option::Option { + return self.transaction_details; + } + + /// Returns the highest transaction version the caller declares it can consume. + #[must_use] + pub const fn max_supported_transaction_version(&self) -> std::option::Option { + return self.max_supported_transaction_version; + } + + /// Returns whether rewards were explicitly requested or suppressed. + #[must_use] + pub const fn rewards(&self) -> std::option::Option { + return self.rewards; + } + + /// Returns whether this config would serialize to an empty object. + pub(crate) const fn is_empty(&self) -> bool { + return self.commitment.is_none() + && self.encoding.is_none() + && self.transaction_details.is_none() + && self.max_supported_transaction_version.is_none() + && self.rewards.is_none(); + } + + /// Serializes this config to the exact Solana JSON-RPC object. + #[must_use] + 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())); + } + if let std::option::Option::Some(encoding) = self.encoding { + object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned())); + } + if let std::option::Option::Some(details) = self.transaction_details { + object.insert("transactionDetails".to_owned(), serde_json::Value::String(details.as_str().to_owned())); + } + if let std::option::Option::Some(version) = self.max_supported_transaction_version { + object.insert("maxSupportedTransactionVersion".to_owned(), serde_json::Value::Number(version.into())); + } + if let std::option::Option::Some(rewards) = self.rewards { + object.insert("rewards".to_owned(), serde_json::Value::Bool(rewards)); + } + return serde_json::Value::Object(object); + } +} + +/// Slot range accepted inside `getBlockProduction` configuration. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SolanaBlockProductionRange { + first_slot: u64, + last_slot: std::option::Option, +} + +impl SolanaBlockProductionRange { + /// Creates a block-production range. + #[must_use] + pub const fn new(first_slot: u64, last_slot: std::option::Option) -> Self { + return Self { first_slot, last_slot }; + } + + /// Returns the first slot in the requested range. + #[must_use] + pub const fn first_slot(&self) -> u64 { + return self.first_slot; + } + + /// Returns the optional last slot in the requested range. + #[must_use] + pub const fn last_slot(&self) -> std::option::Option { + return self.last_slot; + } + + /// Serializes this range to the exact Solana JSON-RPC object. + #[must_use] + pub(crate) fn to_json_value(self) -> serde_json::Value { + let mut object = serde_json::Map::new(); + object.insert("firstSlot".to_owned(), serde_json::Value::Number(self.first_slot.into())); + if let std::option::Option::Some(last_slot) = self.last_slot { + object.insert("lastSlot".to_owned(), serde_json::Value::Number(last_slot.into())); + } + return serde_json::Value::Object(object); + } +} + +/// Optional configuration accepted by `getBlockProduction`. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SolanaBlockProductionConfig { + commitment: std::option::Option, + identity: std::option::Option, + range: std::option::Option, +} + +impl SolanaBlockProductionConfig { + /// Creates a block-production configuration. + #[must_use] + pub const fn new( + commitment: std::option::Option, + identity: std::option::Option, + range: std::option::Option, + ) -> Self { + return Self { commitment, identity, range }; + } + + /// Returns the optional commitment level. + #[must_use] + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } + + /// 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(); + } + + /// Returns the optional slot range. + #[must_use] + pub const fn range(&self) -> std::option::Option<&crate::SolanaBlockProductionRange> { + return self.range.as_ref(); + } + + /// Returns whether this config would serialize to an empty object. + pub(crate) const fn is_empty(&self) -> bool { + return self.commitment.is_none() && self.identity.is_none() && self.range.is_none(); + } + + /// Serializes this config to the exact Solana JSON-RPC object. + #[must_use] + 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())); + } + if let std::option::Option::Some(identity) = self.identity.as_ref() { + object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string())); + } + if let std::option::Option::Some(range) = self.range { + object.insert("range".to_owned(), range.to_json_value()); + } + return serde_json::Value::Object(object); + } +} + +/// Commitment distribution returned by `getBlockCommitment`. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SolanaBlockCommitment { + commitment: std::option::Option>, + total_stake: u64, +} + +impl SolanaBlockCommitment { + /// Returns the nullable commitment stake distribution. + #[must_use] + pub fn commitment(&self) -> std::option::Option<&[u64]> { + return self.commitment.as_deref(); + } + + /// Returns the total active stake considered by the response. + #[must_use] + pub const fn total_stake(&self) -> u64 { + return self.total_stake; + } + + /// Decodes a block-commitment result from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => std::result::Result::Ok(Self { commitment: wire.commitment, total_stake: wire.total_stake }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +/// Effective range reported inside `getBlockProduction` results. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SolanaBlockProductionResultRange { + first_slot: u64, + last_slot: u64, +} + +impl SolanaBlockProductionResultRange { + /// Returns the first slot in the effective range. + #[must_use] + pub const fn first_slot(&self) -> u64 { + return self.first_slot; + } + + /// Returns the last slot in the effective range. + #[must_use] + pub const fn last_slot(&self) -> u64 { + return self.last_slot; + } +} + +/// Block-production counts returned by `getBlockProduction` before the shared RPC context wrapper is applied. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SolanaBlockProduction { + by_identity: std::collections::BTreeMap, + range: crate::SolanaBlockProductionResultRange, +} + +impl SolanaBlockProduction { + /// Returns validator identities mapped to `(leader slots, blocks produced)` counts. + #[must_use] + pub const fn by_identity(&self) -> &std::collections::BTreeMap { + return &self.by_identity; + } + + /// Returns the effective slot range reported by the runtime. + #[must_use] + pub const fn range(&self) -> &crate::SolanaBlockProductionResultRange { + return &self.range; + } + + /// Decodes a block-production result from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => wire, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut by_identity = std::collections::BTreeMap::new(); + for (identity, counts) in wire.by_identity { + let parsed = crate::parse_wire_pubkey(method, "byIdentity", identity.as_str()); + let identity = match parsed { + std::result::Result::Ok(identity) => identity, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + by_identity.insert(identity, counts); + } + return std::result::Result::Ok(Self { + by_identity, + range: crate::SolanaBlockProductionResultRange { first_slot: wire.range.first_slot, last_slot: wire.range.last_slot }, + }); + } +} + +/// One reward entry returned in a confirmed block. +#[derive(Clone, Debug, PartialEq)] +pub struct SolanaBlockReward { + pubkey: ksp_core_lib::Pubkey, + lamports: i64, + post_balance: u64, + reward_type: std::option::Option, + commission: std::option::Option, + commission_bps: crate::SolanaWireField, +} + +impl SolanaBlockReward { + /// Returns the rewarded account public key. + #[must_use] + pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { + return &self.pubkey; + } + + /// Returns the signed lamport delta applied by the reward. + #[must_use] + pub const fn lamports(&self) -> i64 { + return self.lamports; + } + + /// Returns the post-reward account balance in lamports. + #[must_use] + pub const fn post_balance(&self) -> u64 { + return self.post_balance; + } + + /// Returns the runtime reward type text without imposing a local economic taxonomy. + #[must_use] + pub fn reward_type(&self) -> std::option::Option<&str> { + return self.reward_type.as_deref(); + } + + /// Returns the nullable legacy/effective commission percentage. + #[must_use] + pub const fn commission(&self) -> std::option::Option { + return self.commission; + } + + /// Returns the basis-point commission while preserving omitted/null/present wire states. + #[must_use] + pub const fn commission_bps(&self) -> &crate::SolanaWireField { + return &self.commission_bps; + } + + 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 { + std::result::Result::Ok(wire) => wire, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let pubkey = crate::parse_wire_pubkey(method, "rewards.pubkey", wire.pubkey.as_str()); + return match pubkey { + std::result::Result::Ok(pubkey) => std::result::Result::Ok(Self { + pubkey, + lamports: wire.lamports, + post_balance: wire.post_balance, + reward_type: wire.reward_type, + commission: wire.commission, + commission_bps: wire.commission_bps, + }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +/// One transaction entry returned inside a confirmed block. +#[derive(Clone, Debug, PartialEq)] +pub struct SolanaBlockTransaction { + transaction: crate::SolanaEncodedTransaction, + meta: crate::SolanaWireField, + version: crate::SolanaWireField, +} + +impl SolanaBlockTransaction { + /// Returns the encoded or JSON transaction payload without Program-specific decoding. + #[must_use] + pub const fn transaction(&self) -> &crate::SolanaEncodedTransaction { + return &self.transaction; + } + + /// Returns transaction metadata while preserving omission versus explicit `null`. + #[must_use] + pub const fn meta(&self) -> &crate::SolanaWireField { + return &self.meta; + } + + /// Returns the transaction version while preserving omission versus explicit `null`. + #[must_use] + pub const fn version(&self) -> &crate::SolanaWireField { + return &self.version; + } + + 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 { + std::result::Result::Ok(wire) => wire, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let transaction = crate::SolanaEncodedTransaction::decode_wire(method, wire.transaction); + let transaction = match transaction { + std::result::Result::Ok(transaction) => transaction, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let version = decode_block_transaction_version(method, wire.version); + return match version { + std::result::Result::Ok(version) => std::result::Result::Ok(Self { transaction, meta: wire.meta, version }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +/// Confirmed block wire result returned by `getBlock` when the RPC result is non-null. +#[derive(Clone, Debug, PartialEq)] +pub struct SolanaConfirmedBlock { + previous_blockhash: std::string::String, + blockhash: std::string::String, + parent_slot: u64, + transactions: crate::SolanaWireField>, + signatures: crate::SolanaWireField>, + rewards: crate::SolanaWireField>, + num_reward_partitions: crate::SolanaWireField, + block_time: std::option::Option, + block_height: std::option::Option, +} + +impl SolanaConfirmedBlock { + /// Returns the previous blockhash exactly as reported by the RPC wire. + #[must_use] + pub fn previous_blockhash(&self) -> &str { + return self.previous_blockhash.as_str(); + } + + /// Returns this block's blockhash exactly as reported by the RPC wire. + #[must_use] + pub fn blockhash(&self) -> &str { + return self.blockhash.as_str(); + } + + /// Returns the parent slot. + #[must_use] + pub const fn parent_slot(&self) -> u64 { + return self.parent_slot; + } + + /// Returns block transactions while preserving omitted/null/present states. + #[must_use] + pub const fn transactions(&self) -> &crate::SolanaWireField> { + return &self.transactions; + } + + /// Returns block signatures while preserving omitted/null/present states. + #[must_use] + pub const fn signatures(&self) -> &crate::SolanaWireField> { + return &self.signatures; + } + + /// Returns block rewards while preserving omitted/null/present states. + #[must_use] + pub const fn rewards(&self) -> &crate::SolanaWireField> { + return &self.rewards; + } + + /// Returns the SIMD-0118 reward-partition count while preserving omission versus explicit `null`. + #[must_use] + pub const fn num_reward_partitions(&self) -> &crate::SolanaWireField { + return &self.num_reward_partitions; + } + + /// Returns the nullable block time as a Unix timestamp. + #[must_use] + pub const fn block_time(&self) -> std::option::Option { + return self.block_time; + } + + /// Returns the nullable block height. + #[must_use] + pub const fn block_height(&self) -> std::option::Option { + return self.block_height; + } + + /// Decodes a confirmed block from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => wire, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let transactions = decode_block_transactions(method, wire.transactions); + let transactions = match transactions { + std::result::Result::Ok(transactions) => transactions, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let rewards = decode_block_rewards(method, wire.rewards); + let rewards = match rewards { + std::result::Result::Ok(rewards) => rewards, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(Self { + previous_blockhash: wire.previous_blockhash, + blockhash: wire.blockhash, + parent_slot: wire.parent_slot, + transactions, + signatures: wire.signatures, + rewards, + num_reward_partitions: wire.num_reward_partitions, + block_time: wire.block_time, + block_height: wire.block_height, + }); + } +} + +/// One entry returned by `getRecentPerformanceSamples`. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SolanaPerformanceSample { + slot: u64, + num_transactions: u64, + num_non_vote_transactions: std::option::Option, + num_slots: u64, + sample_period_secs: u16, +} + +impl SolanaPerformanceSample { + /// Returns the sample's highest slot. + #[must_use] + pub const fn slot(&self) -> u64 { + return self.slot; + } + + /// Returns the total transaction count in the sample. + #[must_use] + pub const fn num_transactions(&self) -> u64 { + return self.num_transactions; + } + + /// Returns the optional non-vote transaction count exposed by newer runtimes. + #[must_use] + pub const fn num_non_vote_transactions(&self) -> std::option::Option { + return self.num_non_vote_transactions; + } + + /// Returns the number of slots covered by the sample. + #[must_use] + pub const fn num_slots(&self) -> u64 { + return self.num_slots; + } + + /// Returns the sample duration in seconds. + #[must_use] + pub const fn sample_period_secs(&self) -> u16 { + return self.sample_period_secs; + } + + /// Decodes one performance sample from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => std::result::Result::Ok(Self { + slot: wire.slot, + num_transactions: wire.num_transactions, + num_non_vote_transactions: wire.num_non_vote_transactions, + num_slots: wire.num_slots, + sample_period_secs: wire.sample_period_secs, + }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +fn decode_block_transaction_version( + method: &str, + field: crate::SolanaWireField, +) -> ksp_core_lib::Result> { + return match field { + crate::SolanaWireField::Omitted => std::result::Result::Ok(crate::SolanaWireField::Omitted), + crate::SolanaWireField::Null => std::result::Result::Ok(crate::SolanaWireField::Null), + crate::SolanaWireField::Value(value) => { + let decoded = crate::SolanaTransactionVersion::decode_wire(method, value); + match decoded { + std::result::Result::Ok(version) => std::result::Result::Ok(crate::SolanaWireField::Value(version)), + std::result::Result::Err(error) => std::result::Result::Err(error), + } + }, + }; +} + +fn decode_block_transactions( + method: &str, + field: crate::SolanaWireField>, +) -> ksp_core_lib::Result>> { + let values = match field { + crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted), + crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null), + crate::SolanaWireField::Value(values) => values, + }; + let mut transactions = std::vec::Vec::with_capacity(values.len()); + for value in values { + let decoded = crate::SolanaBlockTransaction::decode_wire(method, value); + match decoded { + std::result::Result::Ok(transaction) => transactions.push(transaction), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(crate::SolanaWireField::Value(transactions)); +} + +fn decode_block_rewards( + method: &str, + field: crate::SolanaWireField>, +) -> ksp_core_lib::Result>> { + let values = match field { + crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted), + crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null), + crate::SolanaWireField::Value(values) => values, + }; + let mut rewards = std::vec::Vec::with_capacity(values.len()); + for value in values { + let decoded = crate::SolanaBlockReward::decode_wire(method, value); + match decoded { + std::result::Result::Ok(reward) => rewards.push(reward), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(crate::SolanaWireField::Value(rewards)); +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireBlockCommitment { + commitment: std::option::Option>, + total_stake: u64, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireBlockProductionRange { + first_slot: u64, + last_slot: u64, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireBlockProduction { + by_identity: std::collections::BTreeMap, + range: WireBlockProductionRange, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireBlockReward { + pubkey: std::string::String, + lamports: i64, + post_balance: u64, + #[serde(default)] + reward_type: std::option::Option, + #[serde(default)] + commission: std::option::Option, + #[serde(default)] + commission_bps: crate::SolanaWireField, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireBlockTransaction { + transaction: serde_json::Value, + #[serde(default)] + meta: crate::SolanaWireField, + #[serde(default)] + version: crate::SolanaWireField, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireConfirmedBlock { + previous_blockhash: std::string::String, + blockhash: std::string::String, + parent_slot: u64, + #[serde(default)] + transactions: crate::SolanaWireField>, + #[serde(default)] + signatures: crate::SolanaWireField>, + #[serde(default)] + rewards: crate::SolanaWireField>, + #[serde(default)] + num_reward_partitions: crate::SolanaWireField, + block_time: std::option::Option, + block_height: std::option::Option, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WirePerformanceSample { + slot: u64, + num_transactions: u64, + #[serde(default)] + num_non_vote_transactions: std::option::Option, + num_slots: u64, + sample_period_secs: u16, +} + +#[cfg(test)] +#[path = "../unit_tests/rpc_blocks.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/src/rpc_economics.rs b/crates/ksp-onchain-transport-lib/src/rpc_economics.rs new file mode 100644 index 0000000..e30b4c7 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/src/rpc_economics.rs @@ -0,0 +1,377 @@ +// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs +// version: 1 + +/// Inflation-governor values returned by `getInflationGovernor`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SolanaInflationGovernor { + initial: f64, + terminal: f64, + taper: f64, + foundation: f64, + foundation_term: f64, +} + +impl SolanaInflationGovernor { + /// Returns the initial inflation rate. + #[must_use] + pub const fn initial(&self) -> f64 { + return self.initial; + } + + /// Returns the terminal inflation rate. + #[must_use] + pub const fn terminal(&self) -> f64 { + return self.terminal; + } + + /// Returns the taper/disinflation parameter reported by the runtime. + #[must_use] + pub const fn taper(&self) -> f64 { + return self.taper; + } + + /// Returns the foundation allocation rate. + #[must_use] + pub const fn foundation(&self) -> f64 { + return self.foundation; + } + + /// Returns the foundation allocation term. + #[must_use] + pub const fn foundation_term(&self) -> f64 { + return self.foundation_term; + } + + /// Decodes an inflation-governor result from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => std::result::Result::Ok(Self { + initial: wire.initial, + terminal: wire.terminal, + taper: wire.taper, + foundation: wire.foundation, + foundation_term: wire.foundation_term, + }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +/// Current inflation-rate values returned by `getInflationRate`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SolanaInflationRate { + total: f64, + validator: f64, + foundation: f64, + epoch: u64, +} + +impl SolanaInflationRate { + /// Returns the total inflation rate. + #[must_use] + pub const fn total(&self) -> f64 { + return self.total; + } + + /// Returns the validator inflation rate. + #[must_use] + pub const fn validator(&self) -> f64 { + return self.validator; + } + + /// Returns the foundation inflation rate. + #[must_use] + pub const fn foundation(&self) -> f64 { + return self.foundation; + } + + /// Returns the epoch associated with the rate. + #[must_use] + pub const fn epoch(&self) -> u64 { + return self.epoch; + } + + /// Decodes an inflation-rate result from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => { + std::result::Result::Ok(Self { total: wire.total, validator: wire.validator, foundation: wire.foundation, epoch: wire.epoch }) + }, + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +/// Optional epoch/context configuration accepted by `getInflationReward`. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct SolanaInflationRewardConfig { + epoch: std::option::Option, + commitment: std::option::Option, + min_context_slot: std::option::Option, +} + +impl SolanaInflationRewardConfig { + /// Creates an inflation-reward configuration. + #[must_use] + pub const fn new( + epoch: std::option::Option, + commitment: std::option::Option, + min_context_slot: std::option::Option, + ) -> Self { + return Self { epoch, commitment, min_context_slot }; + } + + /// Returns the optional target epoch. + #[must_use] + pub const fn epoch(&self) -> std::option::Option { + return self.epoch; + } + + /// Returns the optional commitment level. + #[must_use] + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } + + /// Returns the optional minimum context slot. + #[must_use] + pub const fn min_context_slot(&self) -> std::option::Option { + return self.min_context_slot; + } + + /// Returns whether this config would serialize to an empty object. + pub(crate) const fn is_empty(&self) -> bool { + return self.epoch.is_none() && self.commitment.is_none() && self.min_context_slot.is_none(); + } + + /// Serializes this config to the exact Solana JSON-RPC object. + #[must_use] + pub(crate) fn to_json_value(self) -> serde_json::Value { + let mut object = serde_json::Map::new(); + if let std::option::Option::Some(epoch) = self.epoch { + object.insert("epoch".to_owned(), serde_json::Value::Number(epoch.into())); + } + if let std::option::Option::Some(commitment) = self.commitment { + object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); + } + if let std::option::Option::Some(min_context_slot) = self.min_context_slot { + object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into())); + } + return serde_json::Value::Object(object); + } +} + +/// One non-null positional reward returned by `getInflationReward`. +#[derive(Clone, Debug, PartialEq)] +pub struct SolanaInflationReward { + epoch: u64, + effective_slot: u64, + amount: u64, + post_balance: u64, + commission: std::option::Option, + commission_bps: crate::SolanaWireField, +} + +impl SolanaInflationReward { + /// Returns the rewarded epoch. + #[must_use] + pub const fn epoch(&self) -> u64 { + return self.epoch; + } + + /// Returns the slot at which the reward became effective. + #[must_use] + pub const fn effective_slot(&self) -> u64 { + return self.effective_slot; + } + + /// Returns the reward amount in lamports. + #[must_use] + pub const fn amount(&self) -> u64 { + return self.amount; + } + + /// Returns the post-reward account balance in lamports. + #[must_use] + pub const fn post_balance(&self) -> u64 { + return self.post_balance; + } + + /// Returns the nullable legacy/effective commission percentage. + #[must_use] + pub const fn commission(&self) -> std::option::Option { + return self.commission; + } + + /// Returns the basis-point commission while preserving omitted/null/present wire states. + #[must_use] + pub const fn commission_bps(&self) -> &crate::SolanaWireField { + return &self.commission_bps; + } + + /// Decodes one non-null inflation reward from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => std::result::Result::Ok(Self { + epoch: wire.epoch, + effective_slot: wire.effective_slot, + amount: wire.amount, + post_balance: wire.post_balance, + commission: wire.commission, + commission_bps: wire.commission_bps, + }), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + } +} + +/// Optional configuration accepted by `getSupply`. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct SolanaSupplyConfig { + commitment: std::option::Option, + exclude_non_circulating_accounts_list: std::option::Option, +} + +impl SolanaSupplyConfig { + /// Creates a supply configuration. + #[must_use] + pub const fn new(commitment: std::option::Option, exclude_non_circulating_accounts_list: std::option::Option) -> Self { + return Self { commitment, exclude_non_circulating_accounts_list }; + } + + /// Returns the optional commitment level. + #[must_use] + pub const fn commitment(&self) -> std::option::Option { + return self.commitment; + } + + /// Returns whether the request explicitly controls omission of the non-circulating account list. + #[must_use] + pub const fn exclude_non_circulating_accounts_list(&self) -> std::option::Option { + return self.exclude_non_circulating_accounts_list; + } + + /// Returns whether this config would serialize to an empty object. + pub(crate) const fn is_empty(&self) -> bool { + return self.commitment.is_none() && self.exclude_non_circulating_accounts_list.is_none(); + } + + /// Serializes this config to the exact Solana JSON-RPC object. + #[must_use] + 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())); + } + if let std::option::Option::Some(exclude) = self.exclude_non_circulating_accounts_list { + object.insert("excludeNonCirculatingAccountsList".to_owned(), serde_json::Value::Bool(exclude)); + } + return serde_json::Value::Object(object); + } +} + +/// Supply totals returned inside the contextual `getSupply` response. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SolanaSupply { + total: u64, + circulating: u64, + non_circulating: u64, + non_circulating_accounts: std::vec::Vec, +} + +impl SolanaSupply { + /// Returns the total supply in lamports. + #[must_use] + pub const fn total(&self) -> u64 { + return self.total; + } + + /// Returns the circulating supply in lamports. + #[must_use] + pub const fn circulating(&self) -> u64 { + return self.circulating; + } + + /// Returns the non-circulating supply in lamports. + #[must_use] + pub const fn non_circulating(&self) -> u64 { + return self.non_circulating; + } + + /// Returns the ordered non-circulating account list supplied by the runtime. + #[must_use] + pub fn non_circulating_accounts(&self) -> &[ksp_core_lib::Pubkey] { + return self.non_circulating_accounts.as_slice(); + } + + /// Decodes a supply result from its Solana JSON wire shape. + 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 { + std::result::Result::Ok(wire) => wire, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut accounts = std::vec::Vec::with_capacity(wire.non_circulating_accounts.len()); + for account in wire.non_circulating_accounts { + let parsed = crate::parse_wire_pubkey(method, "nonCirculatingAccounts", account.as_str()); + match parsed { + std::result::Result::Ok(pubkey) => accounts.push(pubkey), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(Self { + total: wire.total, + circulating: wire.circulating, + non_circulating: wire.non_circulating, + non_circulating_accounts: accounts, + }); + } +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireInflationGovernor { + initial: f64, + terminal: f64, + taper: f64, + foundation: f64, + foundation_term: f64, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireInflationRate { + total: f64, + validator: f64, + foundation: f64, + epoch: u64, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireInflationReward { + epoch: u64, + effective_slot: u64, + amount: u64, + post_balance: u64, + #[serde(default)] + commission: std::option::Option, + #[serde(default)] + commission_bps: crate::SolanaWireField, +} + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireSupply { + total: u64, + circulating: u64, + non_circulating: u64, + non_circulating_accounts: std::vec::Vec, +} + +#[cfg(test)] +#[path = "../unit_tests/rpc_economics.rs"] +mod tests; diff --git a/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs b/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs index 23f54a0..9aba051 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs -// version: 7 +// version: 8 /// Binary encoding accepted for serialized transaction input payloads. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -838,7 +838,8 @@ pub enum SolanaTransactionVersion { } impl SolanaTransactionVersion { - fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { + /// Decodes a transaction version from the shared Solana JSON wire representation. + pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { return match value { serde_json::Value::String(value) if value == "legacy" => std::result::Result::Ok(Self::Legacy), serde_json::Value::Number(value) => { diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 29a9416..506db7c 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 15 +// version: 16 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -347,3 +347,42 @@ fn public_transaction_pre_007_simulation_wrapper_and_complete_config_are_availab std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd), ); } + +#[test] +fn public_v0_2_4_pre_002_blocks_economics_types_are_available_from_crate_root() { + let block = ksp_onchain_transport_lib::SolanaGetBlockConfig::new( + std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), + std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Json), + std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full), + std::option::Option::Some(1), + std::option::Option::Some(true), + ); + assert_eq!(block.max_supported_transaction_version(), std::option::Option::Some(1)); + let range = ksp_onchain_transport_lib::SolanaBlockProductionRange::new(100, std::option::Option::Some(200)); + assert_eq!(range.last_slot(), std::option::Option::Some(200)); + let production_config = ksp_onchain_transport_lib::SolanaBlockProductionConfig::new( + std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), + std::option::Option::None, + std::option::Option::Some(range), + ); + assert_eq!(production_config.range().map(ksp_onchain_transport_lib::SolanaBlockProductionRange::first_slot), std::option::Option::Some(100)); + let inflation = ksp_onchain_transport_lib::SolanaInflationRewardConfig::new( + std::option::Option::Some(812), + std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized), + std::option::Option::Some(430_000_000), + ); + assert_eq!(inflation.epoch(), std::option::Option::Some(812)); + let supply = ksp_onchain_transport_lib::SolanaSupplyConfig::new(std::option::Option::None, std::option::Option::Some(false)); + assert_eq!(supply.exclude_non_circulating_accounts_list(), std::option::Option::Some(false)); + let _block_commitment: std::option::Option = std::option::Option::None; + let _block_production: std::option::Option = std::option::Option::None; + let _block_production_result_range: std::option::Option = std::option::Option::None; + let _block_reward: std::option::Option = std::option::Option::None; + let _block_transaction: std::option::Option = std::option::Option::None; + let _confirmed_block: std::option::Option = std::option::Option::None; + let _performance_sample: std::option::Option = std::option::Option::None; + let _governor: std::option::Option = std::option::Option::None; + let _rate: std::option::Option = std::option::Option::None; + let _reward: std::option::Option = std::option::Option::None; + let _supply: std::option::Option = std::option::Option::None; +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 3a2aaad..1d60fbf 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,7 +1,7 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 13 +// version: 14 -//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract. +//! Release-level completeness canaries for the staged HTTP wrapper sequence. #[test] fn release_registry_partition_matches_the_audited_http_plan() { @@ -430,3 +430,44 @@ fn release_pre_008_ksp_transport_007_retro_audit_covers_all_typed_current_method let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist"); assert!(get_transaction.request_form_status().has_deprecated_legacy()); } + +#[test] +fn release_v0_2_4_descriptor_set_is_exact_and_remains_read_retry_safe_before_wrappers() { + let mut expected_blocks = std::vec![ + "getBlock", + "getBlockCommitment", + "getBlockHeight", + "getBlockProduction", + "getBlocks", + "getBlocksWithLimit", + "getBlockTime", + "getFirstAvailableBlock", + "getRecentPerformanceSamples", + "minimumLedgerSlot", + ]; + let mut expected_economics = std::vec!["getInflationGovernor", "getInflationRate", "getInflationReward", "getStakeMinimumDelegation", "getSupply",]; + let mut actual_blocks = std::vec::Vec::new(); + let mut actual_economics = std::vec::Vec::new(); + for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() { + if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4 { + continue; + } + assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read); + assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe); + match descriptor.category() { + ksp_onchain_transport_lib::HttpRpcCategory::Blocks => actual_blocks.push(descriptor.method()), + ksp_onchain_transport_lib::HttpRpcCategory::Economics => actual_economics.push(descriptor.method()), + _ => {}, + } + } + actual_blocks.sort_unstable(); + actual_economics.sort_unstable(); + expected_blocks.sort_unstable(); + expected_economics.sort_unstable(); + assert_eq!(actual_blocks, expected_blocks); + assert_eq!(actual_economics, expected_economics); + assert_eq!(actual_blocks.len(), 10); + assert_eq!(actual_economics.len(), 5); + let get_block = ksp_onchain_transport_lib::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist"); + assert!(get_block.request_form_status().has_deprecated_legacy()); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs new file mode 100644 index 0000000..1753ea5 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs @@ -0,0 +1,150 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs +// version: 1 + +#[test] +fn transaction_details_and_get_block_config_preserve_all_modern_options() { + assert_eq!(crate::SolanaTransactionDetails::Full.as_str(), "full"); + assert_eq!(crate::SolanaTransactionDetails::Signatures.as_str(), "signatures"); + assert_eq!(crate::SolanaTransactionDetails::None.as_str(), "none"); + assert_eq!(crate::SolanaTransactionDetails::Accounts.as_str(), "accounts"); + let config = crate::SolanaGetBlockConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Confirmed), + std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed), + std::option::Option::Some(crate::SolanaTransactionDetails::Accounts), + std::option::Option::Some(1), + std::option::Option::Some(false), + ); + assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed)); + assert_eq!(config.encoding(), std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed)); + assert_eq!(config.transaction_details(), std::option::Option::Some(crate::SolanaTransactionDetails::Accounts)); + assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(1)); + assert_eq!(config.rewards(), std::option::Option::Some(false)); + assert_eq!( + config.to_json_value(), + serde_json::json!({ + "commitment":"confirmed", + "encoding":"jsonParsed", + "transactionDetails":"accounts", + "maxSupportedTransactionVersion":1, + "rewards":false + }) + ); + assert!(crate::SolanaGetBlockConfig::default().is_empty()); + assert_eq!(crate::SolanaGetBlockConfig::default().to_json_value(), serde_json::json!({})); +} + +#[test] +fn block_production_config_preserves_identity_range_and_commitment() { + let identity = "11111111111111111111111111111111".parse::().expect("fixture identity must parse"); + let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::Some(430_000_099)); + let config = crate::SolanaBlockProductionConfig::new( + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(identity), + std::option::Option::Some(range), + ); + assert_eq!(config.identity(), std::option::Option::Some(&identity)); + assert_eq!(config.range().expect("range must be present").first_slot(), 430_000_000); + assert_eq!(config.range().expect("range must be present").last_slot(), std::option::Option::Some(430_000_099)); + assert_eq!( + config.to_json_value(), + serde_json::json!({ + "commitment":"finalized", + "identity":"11111111111111111111111111111111", + "range":{"firstSlot":430000000,"lastSlot":430000099} + }) + ); + assert!(crate::SolanaBlockProductionConfig::default().is_empty()); +} + +#[test] +fn block_commitment_fixture_preserves_nullable_distribution() { + let values = serde_json::from_str::>(include_str!("../fixtures/http/block_commitment.variants.json")) + .expect("block commitment fixtures must decode"); + let present = crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", values[0].clone()).expect("present commitment must decode"); + let absent = crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", values[1].clone()).expect("null commitment must decode"); + assert_eq!(present.commitment(), std::option::Option::Some(&[1, 2, 3, 4][..])); + assert_eq!(present.total_stake(), 1_000_000_000); + assert_eq!(absent.commitment(), std::option::Option::None); +} + +#[test] +fn block_production_fixture_types_identity_keys_and_preserves_counts() { + let value = + serde_json::from_str::(include_str!("../fixtures/http/block_production.v4_2_1.json")).expect("block production fixture must decode"); + let result = crate::SolanaBlockProduction::decode_wire("getBlockProduction", value).expect("block production result must decode"); + let identity = "11111111111111111111111111111111".parse::().expect("fixture identity must parse"); + assert_eq!(result.by_identity().get(&identity), std::option::Option::Some(&(8, 7))); + assert_eq!(result.range().first_slot(), 430_000_000); + assert_eq!(result.range().last_slot(), 430_000_099); +} + +#[test] +fn confirmed_block_fixture_preserves_rich_wire_and_simd_extensions() { + let value = + serde_json::from_str::(include_str!("../fixtures/http/confirmed_block.v4_2_1.json")).expect("confirmed block fixture must decode"); + let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value).expect("confirmed block must decode"); + assert_eq!(block.parent_slot(), 430_000_122); + assert_eq!(block.block_time(), std::option::Option::Some(1_787_072_400)); + assert_eq!(block.block_height(), std::option::Option::Some(410_000_000)); + assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4)); + let transactions = block.transactions().value().expect("transactions must be present"); + assert_eq!(transactions.len(), 2); + assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1)))); + assert!(transactions[1].version().is_omitted()); + assert!(transactions[1].meta().is_null()); + let rewards = block.rewards().value().expect("rewards must be present"); + assert_eq!(rewards.len(), 2); + assert_eq!(rewards[0].reward_type(), std::option::Option::Some("Fee")); + assert_eq!(rewards[0].commission(), std::option::Option::None); + assert_eq!(rewards[0].commission_bps().value(), std::option::Option::Some(&1_234)); + assert_eq!(rewards[1].commission(), std::option::Option::Some(5)); + assert!(rewards[1].commission_bps().is_omitted()); +} + +#[test] +fn confirmed_block_fixture_distinguishes_omitted_and_explicit_null_fields() { + let values = serde_json::from_str::>(include_str!("../fixtures/http/confirmed_block.omissions.json")) + .expect("confirmed block omission fixtures must decode"); + let omitted = crate::SolanaConfirmedBlock::decode_wire("getBlock", values[0].clone()).expect("omitted-fields block must decode"); + let nulls = crate::SolanaConfirmedBlock::decode_wire("getBlock", values[1].clone()).expect("null-fields block must decode"); + assert!(omitted.transactions().is_omitted()); + assert!(omitted.signatures().is_omitted()); + assert!(omitted.rewards().is_omitted()); + assert!(omitted.num_reward_partitions().is_omitted()); + assert!(nulls.transactions().is_null()); + assert!(nulls.signatures().is_null()); + assert!(nulls.rewards().is_null()); + assert!(nulls.num_reward_partitions().is_null()); +} + +#[test] +fn performance_sample_fixture_accepts_current_and_older_shapes() { + let values = serde_json::from_str::>(include_str!("../fixtures/http/performance_sample.variants.json")) + .expect("performance sample fixtures must decode"); + let current = crate::SolanaPerformanceSample::decode_wire("getRecentPerformanceSamples", values[0].clone()).expect("current sample must decode"); + let older = crate::SolanaPerformanceSample::decode_wire("getRecentPerformanceSamples", values[1].clone()).expect("older sample must decode"); + assert_eq!(current.slot(), 430_000_123); + assert_eq!(current.num_transactions(), 250_000); + assert_eq!(current.num_non_vote_transactions(), std::option::Option::Some(175_000)); + assert_eq!(current.num_slots(), 120); + assert_eq!(current.sample_period_secs(), 60); + assert_eq!(older.num_non_vote_transactions(), std::option::Option::None); +} + +#[test] +fn block_reward_rejects_invalid_pubkey_without_echoing_value() { + let result = crate::SolanaConfirmedBlock::decode_wire( + "getBlock", + serde_json::json!({ + "previousBlockhash":"previous", + "blockhash":"block", + "parentSlot":1, + "rewards":[{"pubkey":"not-a-pubkey","lamports":1,"postBalance":2,"rewardType":"Fee","commission":null}], + "blockTime":null, + "blockHeight":null + }), + ); + let error = result.expect_err("invalid reward pubkey must fail closed"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); + assert!(!error.to_string().contains("not-a-pubkey")); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs new file mode 100644 index 0000000..1c56458 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs @@ -0,0 +1,86 @@ +// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs +// version: 1 + +#[test] +fn inflation_reward_config_preserves_epoch_commitment_and_min_context_slot() { + let config = crate::SolanaInflationRewardConfig::new( + std::option::Option::Some(811), + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(429_000_000), + ); + assert_eq!(config.epoch(), std::option::Option::Some(811)); + assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized)); + assert_eq!(config.min_context_slot(), std::option::Option::Some(429_000_000)); + assert_eq!(config.to_json_value(), serde_json::json!({"epoch":811,"commitment":"finalized","minContextSlot":429000000})); + assert!(crate::SolanaInflationRewardConfig::default().is_empty()); +} + +#[test] +fn supply_config_distinguishes_omitted_from_explicit_false() { + let omitted = crate::SolanaSupplyConfig::default(); + let explicit_false = crate::SolanaSupplyConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(false)); + assert!(omitted.is_empty()); + assert_eq!(omitted.to_json_value(), serde_json::json!({})); + assert_eq!(explicit_false.exclude_non_circulating_accounts_list(), std::option::Option::Some(false)); + assert_eq!(explicit_false.to_json_value(), serde_json::json!({"commitment":"confirmed","excludeNonCirculatingAccountsList":false})); +} + +#[test] +fn inflation_governor_and_rate_fixtures_preserve_runtime_values() { + let governor_value = serde_json::from_str::(include_str!("../fixtures/http/inflation_governor.v4_2_1.json")) + .expect("inflation governor fixture must decode"); + let rate_value = + serde_json::from_str::(include_str!("../fixtures/http/inflation_rate.v4_2_1.json")).expect("inflation rate fixture must decode"); + let governor = crate::SolanaInflationGovernor::decode_wire("getInflationGovernor", governor_value).expect("inflation governor must decode"); + let rate = crate::SolanaInflationRate::decode_wire("getInflationRate", rate_value).expect("inflation rate must decode"); + assert_eq!(governor.initial(), 0.08); + assert_eq!(governor.terminal(), 0.015); + assert_eq!(governor.taper(), 0.15); + assert_eq!(governor.foundation(), 0.05); + assert_eq!(governor.foundation_term(), 7.0); + assert_eq!(rate.total(), 0.042); + assert_eq!(rate.validator(), 0.041); + assert_eq!(rate.foundation(), 0.001); + assert_eq!(rate.epoch(), 812); +} + +#[test] +fn inflation_reward_fixture_preserves_position_null_and_commission_bps_states() { + let values = serde_json::from_str::>(include_str!("../fixtures/http/inflation_reward.variants.json")) + .expect("inflation reward fixtures must decode"); + let first = crate::SolanaInflationReward::decode_wire("getInflationReward", values[0].clone()).expect("first reward must decode"); + let third = crate::SolanaInflationReward::decode_wire("getInflationReward", values[2].clone()).expect("third reward must decode"); + assert_eq!(values.len(), 3); + assert!(values[1].is_null()); + assert_eq!(first.epoch(), 811); + assert_eq!(first.effective_slot(), 429_900_000); + assert_eq!(first.amount(), 2_500_000); + assert_eq!(first.post_balance(), 1_002_500_000); + assert_eq!(first.commission(), std::option::Option::Some(7)); + assert_eq!(first.commission_bps().value(), std::option::Option::Some(&725)); + assert_eq!(third.commission(), std::option::Option::None); + assert!(third.commission_bps().is_omitted()); +} + +#[test] +fn supply_fixture_types_non_circulating_accounts_and_preserves_order() { + let value = serde_json::from_str::(include_str!("../fixtures/http/supply.v4_2_1.json")).expect("supply fixture must decode"); + let supply = crate::SolanaSupply::decode_wire("getSupply", value).expect("supply must decode"); + assert_eq!(supply.total(), 600_000_000_000_000_000); + assert_eq!(supply.circulating(), 500_000_000_000_000_000); + assert_eq!(supply.non_circulating(), 100_000_000_000_000_000); + assert_eq!(supply.non_circulating_accounts().len(), 2); + assert_eq!(supply.non_circulating_accounts()[0].to_string(), "11111111111111111111111111111111"); + assert_eq!(supply.non_circulating_accounts()[1].to_string(), "Vote111111111111111111111111111111111111111"); +} + +#[test] +fn supply_rejects_invalid_non_circulating_pubkey_without_echoing_value() { + let result = crate::SolanaSupply::decode_wire( + "getSupply", + serde_json::json!({"total":3,"circulating":2,"nonCirculating":1,"nonCirculatingAccounts":["invalid-pubkey"]}), + ); + let error = result.expect_err("invalid supply pubkey must fail closed"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); + assert!(!error.to_string().contains("invalid-pubkey")); +} diff --git a/deltas/0.2.4/pre.002.md b/deltas/0.2.4/pre.002.md new file mode 100644 index 0000000..c10f2d5 --- /dev/null +++ b/deltas/0.2.4/pre.002.md @@ -0,0 +1,331 @@ + + + +# Delta `0.2.4-pre.002` — primitives Blocks/Economics et fixtures wire partagées + +## Base requise + +Livraison précédente : + +```text +0.2.4-pre.001-fix.001 +workspace.package.version = "0.2.4-pre.1" +``` + +Le plan canonique est `docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md` version 2. Le fix `pre.001-fix.001` est appliqué avant cette tranche ; les deltas historiques `pre.001.md` et `pre.001-fix.001.md` restent inchangés. + +## Objectif + +Installer les primitives, configurations et résultats partagés nécessaires aux 10 wrappers Blocks et 5 wrappers Economics de `0.2.4`, ainsi que leurs fixtures wire locales déterministes, sans encore implémenter les wrappers prévus à partir de `pre.003` et sans déclarer la couverture typed `V0_2_4` complète. + +Cette tranche matérialise dès maintenant les exigences `KSP-TRANSPORT-007` qui appartiennent au wire partagé : omissions/null explicites, versions de transaction numériques génériques, reward partitions SIMD-0118, commissions basis-points SIMD-0291, adresses RPC typées en `Pubkey` et sous-arbres Transaction riches conservés losslessly. + +## Version Cargo + +Conformément à `VER-ID-009`, la nouvelle prerelease synchronise le signal technique : + +```text +0.2.4-pre.1 -> 0.2.4-pre.2 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. + +## Implémentation + +### Module Blocks partagé + +Le nouveau module privé `rpc_blocks` ajoute les types publics suivants, réexportés explicitement depuis la racine de crate : + +```text +SolanaTransactionDetails +SolanaGetBlockConfig +SolanaBlockProductionRange +SolanaBlockProductionConfig +SolanaBlockCommitment +SolanaBlockProductionResultRange +SolanaBlockProduction +SolanaBlockReward +SolanaBlockTransaction +SolanaConfirmedBlock +SolanaPerformanceSample +``` + +`SolanaTransactionDetails` couvre exactement les quatre valeurs wire modernes : + +```text +full +signatures +none +accounts +``` + +`SolanaGetBlockConfig` conserve indépendamment les cinq options du contrat moderne : + +```text +commitment +encoding +transactionDetails +maxSupportedTransactionVersion +rewards +``` + +Aucune validation de commitment ni aucun choix de forme legacy n'est encore exécuté ici : ces responsabilités appartiennent au wrapper `getBlock` de `pre.006`. + +`SolanaBlockProductionConfig` conserve `commitment`, une `identity` typée `Pubkey` et une range `firstSlot/lastSlot?`. La validation `lastSlot >= firstSlot` reste volontairement réservée au wrapper `getBlockProduction` de `pre.005`. + +`SolanaBlockProduction` décode les clés dynamiques de `byIdentity` en `Pubkey` et conserve les couples `(leader slots, blocks produced)` ainsi que la range effective retournée par le runtime. Le futur wrapper appliquera le `SolanaRpcResponse` partagé autour de ce résultat. + +### Wire `getBlock` + +`SolanaConfirmedBlock` est un DTO bloc dédié et ne réutilise pas `SolanaConfirmedTransaction`, dont `slot` et `blockTime` sont propres au top-level de `getTransaction`. + +Le bloc conserve : + +```text +previousBlockhash +blockhash +parentSlot +transactions +signatures +rewards +numRewardPartitions +blockTime +blockHeight +``` + +Les champs conditionnels `transactions`, `signatures`, `rewards` et `numRewardPartitions` utilisent `SolanaWireField` afin de préserver les trois états : + +```text +Omitted +Null +Value(T) +``` + +`numRewardPartitions` couvre explicitement le wire associé à SIMD-0118 sans synthétiser une valeur `0` en cas d'omission ou de `null`. + +`SolanaBlockTransaction` compose les primitives Transaction déjà acquises : + +```text +SolanaEncodedTransaction +SolanaWireField pour meta +SolanaWireField pour version +``` + +Le décodeur interne `SolanaTransactionVersion::decode_wire` passe donc de privé à `pub(crate)` pour être réutilisé sans dupliquer la logique `legacy | u8`. Son comportement public reste inchangé. Une fixture `getBlock` porte volontairement la version numérique `1` afin d'empêcher une future régression qui bornerait artificiellement cette primitive à V0, conformément au watchpoint SIMD-0385. + +`SolanaBlockReward` conserve : + +```text +pubkey: Pubkey +lamports: i64 +postBalance: u64 +rewardType: String ou null +commission: u8 ou null +commissionBps: SolanaWireField +``` + +`commissionBps` couvre l'extension stable déjà exposée par le runtime pour SIMD-0291. `commission` et `commissionBps` restent indépendants ; Transport ne dérive jamais l'un depuis l'autre. + +`rewardType` reste volontairement une chaîne nullable au niveau Transport. Cette tranche n'introduit pas une taxonomie économique KSP spéculative alors que les évolutions de reward semantics restent dans la watchlist, notamment SIMD-0123. + +### Module Economics partagé + +Le nouveau module privé `rpc_economics` ajoute et réexporte : + +```text +SolanaInflationGovernor +SolanaInflationRate +SolanaInflationRewardConfig +SolanaInflationReward +SolanaSupplyConfig +SolanaSupply +``` + +`SolanaInflationGovernor` et `SolanaInflationRate` conservent directement les valeurs runtime `f64`/epoch sans reproduire localement les formules d'inflation. + +`SolanaInflationRewardConfig` sérialise uniquement les options fournies : + +```text +epoch +commitment +minContextSlot +``` + +`SolanaInflationReward` conserve `commission: Option` et `commissionBps: SolanaWireField` pour couvrir SIMD-0291 sans perdre la différence omission/null/présent du champ basis-points. La cardinalité et les `null` positionnels de la liste `getInflationReward` seront pris en charge par le wrapper de `pre.008`; le DTO de cette tranche représente uniquement une entrée non-null. + +`SolanaSupplyConfig` utilise `Option` pour `excludeNonCirculatingAccountsList`. Cela distingue un champ omis d'un `false` explicitement envoyé, tout en laissant le runtime appliquer son défaut lorsque l'option est absente. + +`SolanaSupply` convertit la liste ordonnée `nonCirculatingAccounts` en `Vec` et échoue proprement si une clé retournée n'est pas une public key valide. Le futur wrapper réutilisera `SolanaRpcResponse`. + +### Frontières conservées + +Cette prerelease n'ajoute aucun `impl HttpTransportPool` pour les 15 méthodes `V0_2_4`. Les nouveaux types sont donc uniquement des contrats wire/config/result partagés. + +Le flux d'exécution reste inchangé : + +```text +wrapper typed futur + -> descriptor central + -> execute_standard_rpc + -> pool/admission + -> executor HTTP + -> validation JSON-RPC + -> decode typed +``` + +Aucun client HTTP parallèle, aucune boucle de retry locale et aucune dépendance Solana RPC haut niveau ne sont introduits. + +## Fixtures déterministes ajoutées + +```text +crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json +crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json +crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json +crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json +crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json +``` + +Elles couvrent notamment : + +- block commitment présent et `null` ; +- `getBlockProduction.byIdentity` et la range effective ; +- bloc riche avec transaction version numérique `1` ; +- `meta` explicite `null` et version omise ; +- reward `commissionBps` présent puis omis ; +- `numRewardPartitions` présent, omis et explicitement `null` ; +- performance sample courant puis ancien sans `numNonVoteTransactions` ; +- inflation governor/rate ; +- liste inflation reward avec un `null` positionnel et les variantes `commissionBps` ; +- supply avec liste ordonnée de public keys. + +## Tests ajoutés + +`unit_tests/rpc_blocks.rs` ajoute **8 tests déterministes** couvrant configs, résultats, omissions/null, version transaction non nulle, extensions SIMD et validation des public keys. + +`unit_tests/rpc_economics.rs` ajoute **6 tests déterministes** couvrant configs, inflation values, reward basis-points, `null` positionnel de fixture, supply ordonnée et invalid pubkey. + +Les tests d'intégration ajoutent aussi : + +- une canarie de surface publique qui vérifie que les 17 nouveaux types sont disponibles depuis la racine de crate ; +- une canarie de registre qui confirme que `V0_2_4` reste exactement composé des **10 Blocks + 5 Economics**, tous `Read / RetrySafe`, et que `getBlock` conserve son marqueur de forme legacy dépréciée sans prétendre que les wrappers existent déjà. + +La canarie typed-complete `V0_2_4 == 15` reste volontairement fermée jusqu'aux tranches d'implémentation des wrappers. + +## Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/src/rpc_blocks.rs +crates/ksp-onchain-transport-lib/src/rpc_economics.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs +crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json +crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json +crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json +crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json +crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json +crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json +deltas/0.2.4/pre.002.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_transactions.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +``` + +## Fichiers supprimés + +Aucun. + +## Fichiers volontairement inchangés + +```text +CHANGELOG.md +ROADMAP.md +docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md +crates/ksp-onchain-transport-lib/Cargo.toml +crates/ksp-onchain-transport-lib/README.md +crates/ksp-onchain-transport-lib/USAGE.md +crates/ksp-onchain-transport-lib/src/executor.rs +crates/ksp-onchain-transport-lib/src/rpc_method.rs +crates/ksp-config-lib/** +config/** +deltas/0.2.4/pre.001.md +deltas/0.2.4/pre.001-fix.001.md +``` + +Aucun ancien delta n'est réécrit. Aucun wrapper Blocks/Economics n'est avancé prématurément. + +## Validations exécutées dans le sandbox + +- reconstruction exacte de la base `v0.2.3` puis application des overlays `0.2.4-pre.001` et `0.2.4-pre.001-fix.001` ; +- contrôle différentiel de la tranche `pre.002` contre cette base ; +- recoupement ciblé des configs/résultats avec la baseline Agave `v4.2.1` retenue par le plan ; +- parse JSON local des neuf nouvelles fixtures ; +- contrôle statique des 17 réexports crate-root ; +- contrôle statique que chaque nouveau item `pub`/`pub(crate)` de production possède sa documentation ; +- contrôle statique de l'absence de `impl HttpTransportPool` dans les nouveaux modules ; +- contrôle statique de l'absence de `use`, `unwrap`, `expect`, `panic!` et opérateur `?` dans les deux nouvelles sources de production ; +- contrôle statique des lignes Rust modifiées à `<= 160` colonnes ; +- contrôle différentiel de `Cargo.toml` : seul `workspace.package.version` change ; +- contrôle différentiel de `rpc_transactions.rs` : seule la version de fichier et la visibilité interne documentée de `SolanaTransactionVersion::decode_wire` changent ; +- contrôle que `CHANGELOG.md`, `ROADMAP.md`, le plan `011` version 2 et les deltas historiques restent inchangés. + +## Validations Cargo non exécutées + +Le sandbox courant ne fournit pas `cargo`, `rustc` ni `rustfmt`. Les commandes suivantes ne sont donc pas déclarées comme réussies et doivent être exécutées sur le checkout de développement avant commit : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Aucun `cargo tree` supplémentaire n'est requis par cette tranche puisqu'aucune dépendance ni feature n'a changé. Les graphes complets restent obligatoires à la clôture `pre.009`. + +Le commit attendu après application et validations suit `VER-GIT-001` : + +```text +v0.2.4-pre.002 +``` + +## Décisions prises + +- séparer physiquement les primitives Blocks et Economics dans deux modules Transport dédiés ; +- ne pas avancer de wrapper avant sa tranche propriétaire ; +- réutiliser `SolanaWireField`, `SolanaEncodedTransaction` et `SolanaTransactionVersion` plutôt que dupliquer le wire Transaction ; +- ouvrir uniquement la visibilité interne de `SolanaTransactionVersion::decode_wire` sans modifier son API publique ; +- typer les identités/adresses RPC en `Pubkey` lorsqu'elles sont structurelles ; +- garder les sous-arbres Transaction/meta riches lossless via `serde_json::Value` ; +- garder `rewardType` ouvert au niveau Transport au lieu d'introduire une taxonomie économique prématurée ; +- distinguer `excludeNonCirculatingAccountsList` omis d'un `false` explicite ; +- intégrer dès les fixtures partagées SIMD-0118, SIMD-0291 et la canarie future-compatible SIMD-0385 ; +- ne pas modifier le plan `011`, aucune capacité stable additive n'ayant été découverte pendant cette tranche. + +## Questions ouvertes + +Aucune question bloquante pour `pre.003`. + +## Suite + +`0.2.4-pre.003` : implémenter les cinq wrappers Blocks simples avec requêtes JSON exactes et réponses typed : + +```text +getBlockCommitment +getBlockHeight +getBlockTime +getFirstAvailableBlock +minimumLedgerSlot +```