v0.2.2-pre.004
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: could not find account"},"id":1}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireTokenAmount>(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<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireTokenAccountBalance>(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<T> {
|
||||
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<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
|
||||
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaTokenAccountBalance>>> {
|
||||
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<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
|
||||
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
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<serde_json::Value>, 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<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<serde_json::Value>>(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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<serde_json::Value>>>(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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaTokenAccountBalance>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<serde_json::Value>>>(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;
|
||||
|
||||
@@ -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::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let selector = ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(mint);
|
||||
assert!(matches!(selector, ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(_)));
|
||||
}
|
||||
|
||||
@@ -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",],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<std::string::String>) {
|
||||
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::<usize>().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::<ksp_core_lib::Pubkey>().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()]));
|
||||
}
|
||||
|
||||
324
deltas/0.2.2/pre.004.md
Normal file
324
deltas/0.2.2/pre.004.md
Normal file
@@ -0,0 +1,324 @@
|
||||
<!-- file: deltas/0.2.2/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# 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<SolanaCommitmentConfig>
|
||||
-> SolanaRpcResponse<SolanaTokenAmount>
|
||||
```
|
||||
|
||||
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<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaKeyedAccount>>
|
||||
```
|
||||
|
||||
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<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaKeyedAccount>>
|
||||
```
|
||||
|
||||
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<SolanaCommitmentConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaTokenAccountBalance>>
|
||||
```
|
||||
|
||||
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<SolanaCommitmentConfig>
|
||||
-> SolanaRpcResponse<SolanaTokenAmount>
|
||||
```
|
||||
|
||||
`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
|
||||
```
|
||||
Reference in New Issue
Block a user