v0.2.2-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 113
|
# version: 114
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.2.2-pre.3.fix.1"
|
version = "0.2.2-pre.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
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
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![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
|
//! 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 ->
|
//! 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
|
//! 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
|
//! are available. The four typed Solana HTTP foundation canaries plus the `0.2.2` Accounts and Tokens wrappers execute real JSON-RPC requests through
|
||||||
//! transport path while the remaining audited methods stay staged by subsequent `0.2.x` prereleases.
|
//! the shared transport path while the remaining audited methods stay staged by subsequent `0.2.x` prereleases.
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -116,7 +116,7 @@ impl SolanaAccountInfoConfig {
|
|||||||
return self.context.min_context_slot();
|
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();
|
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
|
// file: crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
/// Exclusive selector accepted by token-account list RPC methods.
|
/// Exclusive selector accepted by token-account list RPC methods.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -13,7 +13,6 @@ pub enum SolanaTokenAccountSelector {
|
|||||||
impl SolanaTokenAccountSelector {
|
impl SolanaTokenAccountSelector {
|
||||||
/// Serializes the exclusive selector to the Solana JSON-RPC wire object.
|
/// Serializes the exclusive selector to the Solana JSON-RPC wire object.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||||
return match self {
|
return match self {
|
||||||
Self::Mint(pubkey) => serde_json::json!({"mint": pubkey.to_string()}),
|
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.
|
/// 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> {
|
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);
|
let decoded = crate::decode_wire_json::<WireTokenAmount>(method, value);
|
||||||
return match decoded {
|
return match decoded {
|
||||||
@@ -93,7 +91,6 @@ impl SolanaTokenAccountBalance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes one token-account balance entry from the Solana JSON wire shape.
|
/// 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> {
|
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 decoded = crate::decode_wire_json::<WireTokenAccountBalance>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -115,7 +112,6 @@ impl SolanaTokenAccountBalance {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct WireTokenAmount {
|
struct WireTokenAmount {
|
||||||
amount: std::string::String,
|
amount: std::string::String,
|
||||||
@@ -126,7 +122,6 @@ struct WireTokenAmount {
|
|||||||
ui_amount_string: std::string::String,
|
ui_amount_string: std::string::String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct WireTokenAccountBalance {
|
struct WireTokenAccountBalance {
|
||||||
address: std::string::String,
|
address: std::string::String,
|
||||||
@@ -138,6 +133,222 @@ struct WireTokenAccountBalance {
|
|||||||
ui_amount_string: std::string::String,
|
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)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/rpc_tokens.rs"]
|
#[path = "../unit_tests/rpc_tokens.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// 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.
|
//! 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.domain(), "onchain_transport");
|
||||||
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_INVALID_RPC_PARAMETERS.code(), "invalid_rpc_parameters");
|
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
|
// 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.
|
//! 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",],
|
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
|
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn token_selector_is_exclusive_by_construction() {
|
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.address().to_string(), "11111111111111111111111111111111");
|
||||||
assert_eq!(balance.amount().amount(), "10");
|
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