From 1d1bc6a4d6966fbffc720c6bb442c82dea80a805 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 18 Aug 2026 08:09:20 +0200 Subject: [PATCH] v0.2.2-pre.004 --- Cargo.toml | 4 +- .../http/get_token_account_balance.error.json | 1 + .../get_token_account_balance.success.json | 1 + ...et_token_accounts_by_delegate.success.json | 1 + ...oken_accounts_by_owner.invalid_pubkey.json | 1 + .../get_token_accounts_by_owner.success.json | 1 + ...oken_largest_accounts.invalid_address.json | 1 + .../get_token_largest_accounts.success.json | 1 + .../http/get_token_supply.success.json | 1 + crates/ksp-onchain-transport-lib/src/lib.rs | 6 +- .../src/rpc_accounts.rs | 4 +- .../src/rpc_tokens.rs | 223 +++++++++++- .../tests/public_api.rs | 14 +- .../tests/release_completeness.rs | 21 +- .../unit_tests/rpc_tokens.rs | 226 +++++++++++- deltas/0.2.2/pre.004.md | 324 ++++++++++++++++++ 16 files changed, 814 insertions(+), 16 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json create mode 100644 deltas/0.2.2/pre.004.md diff --git a/Cargo.toml b/Cargo.toml index f54cd11..4c3db8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 113 +# version: 114 [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.3.fix.1" +version = "0.2.2-pre.4" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json new file mode 100644 index 0000000..e690aa7 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: could not find account"},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json new file mode 100644 index 0000000..ccc2416 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000001},"value":{"amount":"18446744073709551615","decimals":9,"uiAmount":null,"uiAmountString":"18446744073.709551615"}},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json new file mode 100644 index 0000000..73fe1e6 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000002},"value":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":{"program":"spl-token","parsed":{"type":"account","info":{"tokenAmount":{"amount":"1","decimals":1,"uiAmount":0.1,"uiAmountString":"0.1"}}},"space":165},"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":9,"space":165}}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json new file mode 100644 index 0000000..25a9f91 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"slot":420000004},"value":[{"pubkey":"not-a-pubkey","account":{"data":["","base64"],"executable":false,"lamports":1,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":0,"space":165}}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json new file mode 100644 index 0000000..5974f6e --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"apiVersion":null,"slot":420000003},"value":[{"pubkey":"Stake11111111111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":10,"space":165}}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json new file mode 100644 index 0000000..4040561 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"slot":420000006},"value":[{"address":"invalid-address","amount":"1","decimals":0,"uiAmount":1.0,"uiAmountString":"1"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json new file mode 100644 index 0000000..222c391 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000005},"value":[{"address":"11111111111111111111111111111111","amount":"9000","decimals":2,"uiAmount":90.0,"uiAmountString":"90"},{"address":"ComputeBudget111111111111111111111111111111","amount":"8000","decimals":2,"uiAmount":80.0,"uiAmountString":"80"}]},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json new file mode 100644 index 0000000..d46a347 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000007},"value":{"amount":"1000000000000000000000000","decimals":6,"uiAmount":1000000000000000000.0,"uiAmountString":"1000000000000000000"}},"id":1} diff --git a/crates/ksp-onchain-transport-lib/src/lib.rs b/crates/ksp-onchain-transport-lib/src/lib.rs index 97170c7..ebefa6a 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: 9 +// version: 10 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -9,8 +9,8 @@ //! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains //! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config -> //! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy -//! are available. The four typed Solana HTTP foundation canaries and the `0.2.2` Accounts wrappers execute real JSON-RPC requests through the shared -//! transport path while the remaining audited methods stay staged by subsequent `0.2.x` prereleases. +//! are available. The four typed Solana HTTP foundation canaries plus the `0.2.2` Accounts and Tokens wrappers execute real JSON-RPC requests through +//! the shared transport path while the remaining audited methods stay staged by subsequent `0.2.x` prereleases. mod client; mod constants; diff --git a/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs b/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs index 99b7a20..985234f 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: 3 +// version: 4 /// Account-data encoding accepted by Solana HTTP account methods. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -116,7 +116,7 @@ impl SolanaAccountInfoConfig { return self.context.min_context_slot(); } - fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { return self.encoding.is_none() && self.data_slice.is_none() && self.commitment().is_none() && self.min_context_slot().is_none(); } diff --git a/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs b/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs index 4b23f66..634ff49 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: 2 +// version: 3 /// Exclusive selector accepted by token-account list RPC methods. #[derive(Clone, Debug, Eq, PartialEq)] @@ -13,7 +13,6 @@ 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()}), @@ -57,7 +56,6 @@ 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 { @@ -93,7 +91,6 @@ 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 { @@ -115,7 +112,6 @@ impl SolanaTokenAccountBalance { } } -#[cfg(test)] #[derive(serde::Deserialize)] struct WireTokenAmount { amount: std::string::String, @@ -126,7 +122,6 @@ struct WireTokenAmount { ui_amount_string: std::string::String, } -#[cfg(test)] #[derive(serde::Deserialize)] struct WireTokenAccountBalance { address: std::string::String, @@ -138,6 +133,222 @@ struct WireTokenAccountBalance { ui_amount_string: std::string::String, } +#[derive(serde::Deserialize)] +struct WireRpcResponse { + context: serde_json::Value, + value: T, +} + +impl crate::HttpTransportPool { + /// Executes typed `getTokenAccountBalance` through the common KSP HTTP transport path. + pub async fn get_token_account_balance( + &self, + role: &crate::HttpRoleName, + token_account: &ksp_core_lib::Pubkey, + config: std::option::Option<&crate::SolanaCommitmentConfig>, + ) -> ksp_core_lib::Result> { + let method_result = token_descriptor("getTokenAccountBalance"); + let method = match method_result { + std::result::Result::Ok(method) => method, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut params = std::vec![serde_json::Value::String(token_account.to_string())]; + push_commitment_config(&mut params, config); + let result = self.execute_standard_rpc(role, method, params).await; + let value = match result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return decode_token_amount_response("getTokenAccountBalance", value); + } + + /// Executes typed `getTokenAccountsByDelegate` through the common KSP HTTP transport path. + pub async fn get_token_accounts_by_delegate( + &self, + role: &crate::HttpRoleName, + delegate: &ksp_core_lib::Pubkey, + selector: &crate::SolanaTokenAccountSelector, + config: std::option::Option<&crate::SolanaAccountInfoConfig>, + ) -> ksp_core_lib::Result>> { + return self.get_token_accounts_list("getTokenAccountsByDelegate", role, delegate, selector, config).await; + } + + /// Executes typed `getTokenAccountsByOwner` through the common KSP HTTP transport path. + pub async fn get_token_accounts_by_owner( + &self, + role: &crate::HttpRoleName, + owner: &ksp_core_lib::Pubkey, + selector: &crate::SolanaTokenAccountSelector, + config: std::option::Option<&crate::SolanaAccountInfoConfig>, + ) -> ksp_core_lib::Result>> { + return self.get_token_accounts_list("getTokenAccountsByOwner", role, owner, selector, config).await; + } + + /// Executes typed `getTokenLargestAccounts` through the common KSP HTTP transport path. + pub async fn get_token_largest_accounts( + &self, + role: &crate::HttpRoleName, + mint: &ksp_core_lib::Pubkey, + config: std::option::Option<&crate::SolanaCommitmentConfig>, + ) -> ksp_core_lib::Result>> { + let method_result = token_descriptor("getTokenLargestAccounts"); + let method = match method_result { + std::result::Result::Ok(method) => method, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut params = std::vec![serde_json::Value::String(mint.to_string())]; + push_commitment_config(&mut params, config); + let result = self.execute_standard_rpc(role, method, params).await; + let value = match result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return decode_token_account_balances_response("getTokenLargestAccounts", value); + } + + /// Executes typed `getTokenSupply` through the common KSP HTTP transport path. + pub async fn get_token_supply( + &self, + role: &crate::HttpRoleName, + mint: &ksp_core_lib::Pubkey, + config: std::option::Option<&crate::SolanaCommitmentConfig>, + ) -> ksp_core_lib::Result> { + let method_result = token_descriptor("getTokenSupply"); + let method = match method_result { + std::result::Result::Ok(method) => method, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut params = std::vec![serde_json::Value::String(mint.to_string())]; + push_commitment_config(&mut params, config); + let result = self.execute_standard_rpc(role, method, params).await; + let value = match result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return decode_token_amount_response("getTokenSupply", value); + } + + async fn get_token_accounts_list( + &self, + method_name: &'static str, + role: &crate::HttpRoleName, + address: &ksp_core_lib::Pubkey, + selector: &crate::SolanaTokenAccountSelector, + config: std::option::Option<&crate::SolanaAccountInfoConfig>, + ) -> ksp_core_lib::Result>> { + let method_result = token_descriptor(method_name); + let method = match method_result { + std::result::Result::Ok(method) => method, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut params = std::vec![serde_json::Value::String(address.to_string()), selector.to_json_value()]; + if let std::option::Option::Some(config) = config + && !config.is_empty() + { + params.push(config.to_json_value()); + } + let result = self.execute_standard_rpc(role, method, params).await; + let value = match result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return decode_keyed_accounts_response(method_name, value); + } +} + +fn token_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { + let descriptor = crate::find_http_rpc_method(method); + return match descriptor { + std::option::Option::Some(descriptor) + if descriptor.category() == crate::HttpRpcCategory::Tokens && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 => + { + std::result::Result::Ok(descriptor) + }, + _ => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Tokens descriptor is missing from the audited 0.2.2 registry") + .with_context("rpc_method", method), + ), + }; +} + +fn push_commitment_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaCommitmentConfig>) { + if let std::option::Option::Some(config) = config + && config.commitment().is_some() + { + params.push(config.to_json_value()); + } + return; +} + +fn decode_token_amount_response(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 context = crate::SolanaRpcContext::decode_wire(method, wire.context); + let context = match context { + std::result::Result::Ok(context) => context, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let amount = crate::SolanaTokenAmount::decode_wire(method, wire.value); + let amount = match amount { + std::result::Result::Ok(amount) => amount, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, amount)); +} + +fn decode_keyed_accounts_response( + 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 context = crate::SolanaRpcContext::decode_wire(method, wire.context); + let context = match context { + std::result::Result::Ok(context) => context, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut accounts = std::vec::Vec::with_capacity(wire.value.len()); + for value in wire.value { + let account = crate::SolanaKeyedAccount::decode_wire(method, value); + match account { + std::result::Result::Ok(account) => accounts.push(account), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, accounts)); +} + +fn decode_token_account_balances_response( + 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 context = crate::SolanaRpcContext::decode_wire(method, wire.context); + let context = match context { + std::result::Result::Ok(context) => context, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let mut accounts = std::vec::Vec::with_capacity(wire.value.len()); + for value in wire.value { + let account = crate::SolanaTokenAccountBalance::decode_wire(method, value); + match account { + std::result::Result::Ok(account) => accounts.push(account), + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, accounts)); +} + #[cfg(test)] #[path = "../unit_tests/rpc_tokens.rs"] mod tests; diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index db51b4c..966cb6e 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: 6 +// version: 7 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -209,3 +209,15 @@ fn public_pre_003_account_wrappers_are_available_from_crate_root() { assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_RPC_PARAMETERS.domain(), "onchain_transport"); assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_RPC_PARAMETERS.code(), "invalid_rpc_parameters"); } + +#[test] +fn public_pre_004_token_wrappers_are_available_from_crate_root() { + let _get_token_account_balance = ksp_onchain_transport_lib::HttpTransportPool::get_token_account_balance; + let _get_token_accounts_by_delegate = ksp_onchain_transport_lib::HttpTransportPool::get_token_accounts_by_delegate; + let _get_token_accounts_by_owner = ksp_onchain_transport_lib::HttpTransportPool::get_token_accounts_by_owner; + let _get_token_largest_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_token_largest_accounts; + let _get_token_supply = ksp_onchain_transport_lib::HttpTransportPool::get_token_supply; + let mint = "11111111111111111111111111111111".parse::().expect("fixture pubkey must parse"); + let selector = ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(mint); + assert!(matches!(selector, ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(_))); +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 3ca347b..1258b26 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 2 +// version: 3 //! Release-level completeness canaries for the `0.2.1` HTTP foundation contract. @@ -65,3 +65,22 @@ fn release_pre_003_accounts_subset_is_exact_without_advancing_other_v0_2_2_famil std::vec!["getAccountInfo", "getLargestAccounts", "getMinimumBalanceForRentExemption", "getMultipleAccounts", "getProgramAccounts",], ); } + +#[test] +fn release_pre_004_tokens_subset_is_exact_and_retry_safe() { + let mut token_names = std::vec::Vec::<&str>::new(); + for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() { + if descriptor.coverage_release() == ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2 + && descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Tokens + { + token_names.push(descriptor.method()); + assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read); + assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe); + } + } + token_names.sort_unstable(); + assert_eq!( + token_names, + std::vec!["getTokenAccountBalance", "getTokenAccountsByDelegate", "getTokenAccountsByOwner", "getTokenLargestAccounts", "getTokenSupply",], + ); +} 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 c9fb237..655d27e 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: 2 +// version: 3 #[test] fn token_selector_is_exclusive_by_construction() { @@ -31,3 +31,227 @@ fn staged_token_account_balance_helper_preserves_address_and_amount() { assert_eq!(balance.address().to_string(), "11111111111111111111111111111111"); assert_eq!(balance.amount().amount(), "10"); } + +fn pool_for_url(url: &str) -> crate::HttpTransportPool { + let role = crate::HttpEndpointRoleSettings::new( + crate::HttpRoleName::new("default"), + true, + std::vec![crate::HttpRequestKind::wildcard()], + 10, + crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), + ); + let endpoint = crate::HttpEndpointSettings::new( + "fixture", + true, + crate::HttpProviderName::new("fixture"), + crate::HttpClusterName::new("local"), + crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"), + std::time::Duration::from_secs(1), + std::time::Duration::from_secs(1), + std::option::Option::Some(1), + std::vec![role], + ); + let settings = crate::HttpTransportSettings::new( + std::vec![endpoint], + crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)), + ); + return crate::HttpTransportPool::new(settings).expect("fixture pool must build"); +} + +fn serve_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle) { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind"); + let address = listener.local_addr().expect("fixture listener address must resolve"); + let handle = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("fixture server must accept one request"); + let request = read_request(&mut stream); + let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body); + std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write"); + return request; + }); + return (format!("http://{address}"), handle); +} + +fn read_request(stream: &mut std::net::TcpStream) -> std::string::String { + let mut bytes = std::vec::Vec::new(); + let mut buffer = [0_u8; 1024]; + loop { + let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read"); + if count == 0 { + break; + } + bytes.extend_from_slice(&buffer[..count]); + if request_complete(bytes.as_slice()) { + break; + } + } + return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8"); +} + +fn request_complete(bytes: &[u8]) -> bool { + let text = match std::str::from_utf8(bytes) { + std::result::Result::Ok(text) => text, + std::result::Result::Err(_) => return false, + }; + let header_end = match text.find("\r\n\r\n") { + std::option::Option::Some(value) => value, + std::option::Option::None => return false, + }; + let mut content_length = 0_usize; + for line in text[..header_end].lines() { + let (name, value) = match line.split_once(':') { + std::option::Option::Some(parts) => parts, + std::option::Option::None => continue, + }; + if name.eq_ignore_ascii_case("content-length") { + content_length = value.trim().parse::().expect("content length must parse"); + } + } + return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length); +} + +fn request_body(request: &str) -> serde_json::Value { + let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist"); + return serde_json::from_str(body).expect("fixture request body must be JSON"); +} + +fn fixture_pubkey(value: &str) -> ksp_core_lib::Pubkey { + return value.parse::().expect("fixture pubkey must parse"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_account_balance_serializes_commitment_and_preserves_nullable_ui_amount() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_account_balance.success.json")); + let pool = pool_for_url(url.as_str()); + let account = fixture_pubkey("11111111111111111111111111111111"); + let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized)); + let response = pool + .get_token_account_balance(&crate::HttpRoleName::new("default"), &account, std::option::Option::Some(&config)) + .await + .expect("token balance fixture must succeed"); + assert_eq!(response.context().slot(), 420_000_001); + assert_eq!(response.value().amount(), "18446744073709551615"); + assert_eq!(response.value().ui_amount(), std::option::Option::None); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getTokenAccountBalance")); + assert_eq!(body["params"], serde_json::json!([account.to_string(),{"commitment":"finalized"}])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_account_balance_preserves_rpc_application_error() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_account_balance.error.json")); + let pool = pool_for_url(url.as_str()); + let account = fixture_pubkey("11111111111111111111111111111111"); + let result = pool.get_token_account_balance(&crate::HttpRoleName::new("default"), &account, std::option::Option::None).await; + let error = result.expect_err("remote invalid token account must remain an RPC application error"); + assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); + handle.join().expect("fixture server must join"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_accounts_by_delegate_serializes_program_selector_and_config() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_accounts_by_delegate.success.json")); + let pool = pool_for_url(url.as_str()); + let delegate = fixture_pubkey("11111111111111111111111111111111"); + let program_id = fixture_pubkey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); + let selector = crate::SolanaTokenAccountSelector::ProgramId(program_id); + let config = crate::SolanaAccountInfoConfig::new( + std::option::Option::Some(crate::SolanaAccountEncoding::JsonParsed), + std::option::Option::None, + std::option::Option::Some(crate::SolanaCommitment::Finalized), + std::option::Option::Some(420_000_000), + ); + let response = pool + .get_token_accounts_by_delegate(&crate::HttpRoleName::new("default"), &delegate, &selector, std::option::Option::Some(&config)) + .await + .expect("delegate token accounts fixture must succeed"); + assert_eq!(response.context().slot(), 420_000_002); + assert_eq!(response.value().len(), 1); + assert!(matches!(response.value()[0].account().data(), crate::SolanaAccountData::JsonParsed(_))); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getTokenAccountsByDelegate")); + assert_eq!(body["params"][0], serde_json::json!(delegate.to_string())); + assert_eq!(body["params"][1], serde_json::json!({"programId":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"})); + assert_eq!(body["params"][2], serde_json::json!({"encoding":"jsonParsed","commitment":"finalized","minContextSlot":420000000})); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_accounts_by_owner_serializes_mint_selector_and_omits_empty_config() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_accounts_by_owner.success.json")); + let pool = pool_for_url(url.as_str()); + let owner = fixture_pubkey("11111111111111111111111111111111"); + let mint = fixture_pubkey("ComputeBudget111111111111111111111111111111"); + let selector = crate::SolanaTokenAccountSelector::Mint(mint); + let config = crate::SolanaAccountInfoConfig::default(); + let response = pool + .get_token_accounts_by_owner(&crate::HttpRoleName::new("default"), &owner, &selector, std::option::Option::Some(&config)) + .await + .expect("owner token accounts fixture must succeed"); + assert_eq!(response.context().api_version(), std::option::Option::None); + assert_eq!(response.value().len(), 1); + assert_eq!(response.value()[0].pubkey().to_string(), "Stake11111111111111111111111111111111111111"); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getTokenAccountsByOwner")); + assert_eq!(body["params"], serde_json::json!([owner.to_string(),{"mint":"ComputeBudget111111111111111111111111111111"}])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_accounts_by_owner_rejects_invalid_account_pubkey_response() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json")); + let pool = pool_for_url(url.as_str()); + let owner = fixture_pubkey("11111111111111111111111111111111"); + let mint = fixture_pubkey("ComputeBudget111111111111111111111111111111"); + let selector = crate::SolanaTokenAccountSelector::Mint(mint); + let result = pool.get_token_accounts_by_owner(&crate::HttpRoleName::new("default"), &owner, &selector, std::option::Option::None).await; + let error = result.expect_err("invalid account pubkey must reject the typed response"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); + handle.join().expect("fixture server must join"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_largest_accounts_decodes_order_and_commitment() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_largest_accounts.success.json")); + let pool = pool_for_url(url.as_str()); + let mint = fixture_pubkey("11111111111111111111111111111111"); + let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed)); + let response = pool + .get_token_largest_accounts(&crate::HttpRoleName::new("default"), &mint, std::option::Option::Some(&config)) + .await + .expect("largest token accounts fixture must succeed"); + assert_eq!(response.value().len(), 2); + assert_eq!(response.value()[0].amount().amount(), "9000"); + assert_eq!(response.value()[1].amount().amount(), "8000"); + let request = handle.join().expect("fixture server must join"); + assert_eq!(request_body(request.as_str())["params"], serde_json::json!([mint.to_string(),{"commitment":"confirmed"}])); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_largest_accounts_rejects_invalid_address() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_largest_accounts.invalid_address.json")); + let pool = pool_for_url(url.as_str()); + let mint = fixture_pubkey("11111111111111111111111111111111"); + let result = pool.get_token_largest_accounts(&crate::HttpRoleName::new("default"), &mint, std::option::Option::None).await; + let error = result.expect_err("invalid token-account address must reject the typed response"); + assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE); + handle.join().expect("fixture server must join"); +} + +#[tokio::test(flavor = "current_thread")] +async fn typed_get_token_supply_preserves_exact_amount_and_omits_absent_config() { + let (url, handle) = serve_once(include_str!("../fixtures/http/get_token_supply.success.json")); + let pool = pool_for_url(url.as_str()); + let mint = fixture_pubkey("11111111111111111111111111111111"); + let response = pool + .get_token_supply(&crate::HttpRoleName::new("default"), &mint, std::option::Option::None) + .await + .expect("token supply fixture must succeed"); + assert_eq!(response.context().slot(), 420_000_007); + assert_eq!(response.value().amount(), "1000000000000000000000000"); + assert_eq!(response.value().ui_amount_string(), "1000000000000000000"); + let request = handle.join().expect("fixture server must join"); + let body = request_body(request.as_str()); + assert_eq!(body["method"], serde_json::json!("getTokenSupply")); + assert_eq!(body["params"], serde_json::json!([mint.to_string()])); +} diff --git a/deltas/0.2.2/pre.004.md b/deltas/0.2.2/pre.004.md new file mode 100644 index 0000000..e441afb --- /dev/null +++ b/deltas/0.2.2/pre.004.md @@ -0,0 +1,324 @@ + + + +# Delta `0.2.2-pre.004` — cinq wrappers HTTP Tokens typés + +## Base requise + +Livraison précédente corrigée et validée localement par l'opérateur : + +```text +0.2.2-pre.003-fix.001 +workspace.package.version = "0.2.2-pre.3.fix.1" +``` + +La validation opérateur du 2026-08-18 a confirmé : + +```text +cargo fmt --all OK +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +cargo test -p ksp-onchain-transport-lib OK +``` + +Résultats Transport de cette base : + +```text +98 unit tests +10 public API tests +3 release completeness tests +0 warning signalé par check/clippy +``` + +Le plan canonique reste `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3. + +## Objectif + +Clôturer la famille Tokens attribuée à `0.2.2` avec cinq wrappers publics typés : + +```text +getTokenAccountBalance +getTokenAccountsByDelegate +getTokenAccountsByOwner +getTokenLargestAccounts +getTokenSupply +``` + +Tous passent par la foundation HTTP commune : + +```text +wrapper typé + -> descriptor audité + -> execute_standard_rpc + -> pool/admission/retry/deadline + -> reqwest HTTP + -> JSON-RPC validation + -> décodage DTO KSP +``` + +Aucun wrapper Token ne contacte `reqwest` directement et aucune crate SPL n'est ajoutée. + +## Version Cargo + +Conformément à `VER-ID-009` : + +```text +0.2.2-pre.3.fix.1 -> 0.2.2-pre.4 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée ou retirée. + +## Réaudit ciblé du contrat courant + +Les pages RPC Solana courantes ont été revérifiées le 2026-08-18 pour cette tranche : + +```text +https://solana.com/docs/rpc/http/gettokenaccountbalance +https://solana.com/docs/rpc/http/gettokenaccountsbydelegate +https://solana.com/docs/rpc/http/gettokenaccountsbyowner +https://solana.com/docs/rpc/http/gettokenlargestaccounts +https://solana.com/docs/rpc/http/gettokensupply +``` + +Le contrat reste cohérent avec le plan `009` : + +- `getTokenAccountBalance`, `getTokenLargestAccounts` et `getTokenSupply` acceptent un `commitment` optionnel ; +- `getTokenAccountsByDelegate` et `getTokenAccountsByOwner` prennent un selector exclusif `{mint}` ou `{programId}` puis une config Account optionnelle ; +- les cinq réponses sont contextualisées ; +- les deux méthodes de liste renvoient des `pubkey + account` et réutilisent donc le DTO Account générique ; +- `TokenAmount.uiAmount` reste nullable ; `uiAmountString` reste la représentation textuelle à préserver ; +- `getTokenLargestAccounts` renvoie les 20 plus gros token accounts côté RPC, sans que KSP invente un tri supplémentaire. + +Le réaudit complémentaire Agave `v4.2.1` déjà fixé en `pre.001-fix.001` reste la référence d'implémentation ciblée ; aucune divergence +nouvelle nécessitant un changement de DTO n'a été identifiée pour cette tranche. + +## Surface typée Tokens + +### `getTokenAccountBalance` + +Signature publique conceptuelle : + +```text +role + token account Pubkey + Option + -> SolanaRpcResponse +``` + +Le wrapper préserve `amount`, `decimals`, `uiAmount` nullable et `uiAmountString`, et conserve une erreur JSON-RPC distante comme erreur +applicative RPC. + +### `getTokenAccountsByDelegate` + +Signature publique conceptuelle : + +```text +role + delegate Pubkey + SolanaTokenAccountSelector + Option + -> SolanaRpcResponse> +``` + +Le selector KSP empêche par construction les objets contenant simultanément `mint` et `programId`. La config Account réutilise les encodings, +`dataSlice`, `commitment` et `minContextSlot` activés en `pre.003`. + +### `getTokenAccountsByOwner` + +Signature publique conceptuelle : + +```text +role + owner Pubkey + SolanaTokenAccountSelector + Option + -> SolanaRpcResponse> +``` + +Le wrapper partage le même chemin privé de sérialisation/décodage que la variante Delegate afin d'éviter deux implémentations de wire parallèles. +Un config Account explicitement vide est omis du tableau `params`. + +### `getTokenLargestAccounts` + +Signature publique conceptuelle : + +```text +role + mint Pubkey + Option + -> SolanaRpcResponse> +``` + +L'ordre retourné par le serveur est conservé. Chaque `address` est validée et convertie en `ksp_core_lib::Pubkey` sans recopier une valeur wire +invalide dans le diagnostic. + +### `getTokenSupply` + +Signature publique conceptuelle : + +```text +role + mint Pubkey + Option + -> SolanaRpcResponse +``` + +`amount` reste une chaîne exacte et KSP ne convertit pas la supply en type métier SPL ni en entier borné localement. + +## Mutualisation activée en production + +Les helpers Token préparés en `pre.002` deviennent production-live uniquement parce qu'ils ont maintenant des consommateurs runtime réels : + +- sérialisation de `SolanaTokenAccountSelector` ; +- décodage `SolanaTokenAmount` ; +- décodage `SolanaTokenAccountBalance` ; +- wire structs Token privés. + +`SolanaAccountInfoConfig::is_empty` devient `pub(crate)` pour permettre aux deux wrappers de liste Token de réutiliser la règle d'omission de +config vide déjà utilisée par Accounts. Cette modification reste interne à la crate et n'élargit pas l'API publique externe. + +Les helpers Cluster préparatoires restent inchangés et ne deviennent pas production-live avant `pre.005`/`pre.006`. + +## Validation locale des paramètres + +Aucun nouveau code d'erreur local n'est nécessaire dans cette tranche : + +- le selector `Mint | ProgramId` garantit exactement une variante par construction ; +- les owner/delegate/mint/token-account sont des `Pubkey` typées avant l'appel ; +- les configs Account/Commitment réutilisent les types déjà validés ; +- aucune cardinalité supplémentaire documentée ne doit être imposée côté client. + +`ERROR_CODE_INVALID_RPC_PARAMETERS` introduit en `pre.003` reste inchangé. + +## Fixtures HTTP déterministes ajoutées + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json +``` + +Les tests utilisent un serveur HTTP loopback et n'accèdent pas à Internet. + +## Couverture de tests ajoutée + +Les tests Token couvrent notamment : + +- request exacte de chacune des cinq méthodes ; +- selector `programId` pour Delegate ; +- selector `mint` pour Owner ; +- config Account complète et omission d'un config explicitement vide ; +- commitment optionnel ; +- `uiAmount: null` ; +- montant/supply textuel exact ; +- conservation de l'ordre pour `getTokenLargestAccounts` ; +- décodage `jsonParsed` des accounts sous forme JSON opaque Transport ; +- erreur JSON-RPC applicative ; +- rejet d'une `pubkey` ou `address` invalide dans une réponse typée. + +Le test public compile explicitement les cinq méthodes depuis `HttpTransportPool`. + +Un canari release fige l'ensemble Tokens `0.2.2` exact : + +```text +getTokenAccountBalance +getTokenAccountsByDelegate +getTokenAccountsByOwner +getTokenLargestAccounts +getTokenSupply +``` + +Il vérifie également `Read + RetrySafe` pour chacun. Les 12 wrappers Cluster restent hors de cette tranche. + +Après application, la cible Transport attendue devient : + +```text +106 unit tests +11 public API tests +4 release completeness tests +``` + +## Fichiers ajoutés + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_account_balance.error.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_delegate.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_accounts_by_owner.invalid_pubkey.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.success.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_largest_accounts.invalid_address.json +crates/ksp-onchain-transport-lib/fixtures/http/get_token_supply.success.json +deltas/0.2.2/pre.004.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/lib.rs +crates/ksp-onchain-transport-lib/src/rpc_accounts.rs +crates/ksp-onchain-transport-lib/src/rpc_tokens.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.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 +deltas/0.2.2/pre.003.md +deltas/0.2.2/pre.003-fix.001.md +docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md +ROADMAP.md +CHANGELOG.md +crates/ksp-onchain-transport-lib/src/rpc_common.rs +crates/ksp-onchain-transport-lib/src/rpc_cluster.rs +crates/ksp-onchain-transport-lib/src/rpc_method.rs +crates/ksp-config-lib/** +config/** +``` + +Les deltas déjà publiés restent des traces historiques et ne sont pas réécrits. + +## Contrôles statiques effectués pendant la préparation + +- `workspace.package.version` vaut `0.2.2-pre.4` ; +- les huit nouvelles fixtures JSON sont syntaxiquement valides ; +- les cinq wrappers utilisent `execute_standard_rpc` ; +- aucun `reqwest` direct n'est ajouté dans `rpc_tokens.rs` ; +- aucun `#[allow(dead_code)]` ou suppression de lint n'est ajouté ; +- aucun helper Cluster n'est activé en production ; +- aucune dépendance ou feature Cargo n'est ajoutée ; +- aucune crate SPL n'est importée ; +- la surface publique Accounts de `pre.003` reste présente ; +- les deltas historiques restent inchangés. + +## Validations Rust non exécutées dans le sandbox + +Le sandbox de préparation ne fournit pas la toolchain Cargo utilisée par le checkout opérateur. Les commandes suivantes doivent donc être rejouées +après application : + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +Les validations ne sont pas déclarées réussies tant qu'elles n'ont pas été exécutées sur le checkout opérateur. + +## Questions ouvertes + +Aucune question bloquante. + +## Suite + +Après validation et commit de `0.2.2-pre.004`, poursuivre avec `0.2.2-pre.005` pour les sept wrappers Cluster simples : + +```text +getClusterNodes +getEpochInfo +getEpochSchedule +getHighestSnapshotSlot +getIdentity +getMaxRetransmitSlot +getMaxShredInsertSlot +```