v0.2.2-pre.003
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 112
|
||||
# version: 113
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.2-pre.2.fix.2"
|
||||
version = "0.2.2-pre.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000003},"value":{"data":["","base64"],"executable":false,"lamports":1,"owner":"not-a-pubkey","rentEpoch":0,"space":0}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000002},"value":null},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000001},"value":{"data":["AQIDBA==","base64"],"executable":false,"lamports":2039280,"owner":"11111111111111111111111111111111","rentEpoch":18446744073709551615,"space":4}},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000005},"value":[{"address":"invalid-address","lamports":1}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000004},"value":[{"address":"11111111111111111111111111111111","lamports":999999999},{"address":"ComputeBudget111111111111111111111111111111","lamports":888888888}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param"},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":890880,"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000006},"value":[{"data":["","base64"],"executable":false,"lamports":10,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":0},null,{"data":{"program":"system","parsed":{"type":"nonce"},"space":80},"executable":false,"lamports":20,"owner":"11111111111111111111111111111111","rentEpoch":1,"space":80}]},"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":42,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":3}}],"id":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000007},"value":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":42,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":3}}]},"id":1}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Error code used when HTTP transport runtime settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings");
|
||||
@@ -25,3 +25,5 @@ pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_l
|
||||
pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed");
|
||||
/// Error code used when a decoded response cannot satisfy the KSP transport contract expected by the caller.
|
||||
pub const ERROR_CODE_INVALID_RESPONSE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_response");
|
||||
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
|
||||
pub const ERROR_CODE_INVALID_RPC_PARAMETERS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_rpc_parameters");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
#![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 first typed Solana HTTP canaries execute real
|
||||
//! JSON-RPC requests while the remaining audited methods stay staged by subsequent `0.2.x` releases.
|
||||
//! 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.
|
||||
|
||||
mod client;
|
||||
mod constants;
|
||||
@@ -45,6 +45,8 @@ pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
|
||||
pub use self::error::ERROR_CODE_HTTP_REQUEST_FAILED;
|
||||
/// Error code used when a decoded response cannot satisfy the expected KSP transport contract.
|
||||
pub use self::error::ERROR_CODE_INVALID_RESPONSE;
|
||||
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
|
||||
pub use self::error::ERROR_CODE_INVALID_RPC_PARAMETERS;
|
||||
/// Error code used when HTTP transport runtime settings are invalid.
|
||||
pub use self::error::ERROR_CODE_INVALID_SETTINGS;
|
||||
/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -29,7 +29,6 @@ impl SolanaAccountEncoding {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn from_wire(value: &str) -> std::option::Option<Self> {
|
||||
return match value {
|
||||
"binary" => std::option::Option::Some(Self::Binary),
|
||||
@@ -68,7 +67,6 @@ impl SolanaDataSliceConfig {
|
||||
return self.length;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn to_json_value(self) -> serde_json::Value {
|
||||
return serde_json::json!({"offset": self.offset, "length": self.length});
|
||||
}
|
||||
@@ -118,9 +116,12 @@ impl SolanaAccountInfoConfig {
|
||||
return self.context.min_context_slot();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let context_value = self.context.to_json_value();
|
||||
let mut object = match context_value {
|
||||
@@ -147,7 +148,6 @@ pub enum SolanaLargestAccountsFilter {
|
||||
}
|
||||
|
||||
impl SolanaLargestAccountsFilter {
|
||||
#[cfg(test)]
|
||||
fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Circulating => "circulating",
|
||||
@@ -193,9 +193,12 @@ impl SolanaLargestAccountsConfig {
|
||||
return self.sort_results;
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
return self.commitment.is_none() && self.filter.is_none() && self.sort_results.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
@@ -248,7 +251,6 @@ impl SolanaMemcmpFilter {
|
||||
return &self.bytes;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match &self.bytes {
|
||||
crate::SolanaMemcmpBytes::Base58(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base58"}),
|
||||
@@ -270,7 +272,6 @@ pub enum SolanaProgramAccountFilter {
|
||||
}
|
||||
|
||||
impl SolanaProgramAccountFilter {
|
||||
#[cfg(test)]
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::DataSize(size) => serde_json::json!({"dataSize": size}),
|
||||
@@ -325,9 +326,12 @@ impl SolanaProgramAccountsConfig {
|
||||
return self.sort_results;
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
return self.account_config.is_empty() && self.filters.is_empty() && self.with_context.is_none() && self.sort_results.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let account_value = self.account_config.to_json_value();
|
||||
let mut object = match account_value {
|
||||
@@ -393,7 +397,6 @@ pub enum SolanaAccountData {
|
||||
}
|
||||
|
||||
impl SolanaAccountData {
|
||||
#[cfg(test)]
|
||||
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccountData>(method, value);
|
||||
let wire = match decoded {
|
||||
@@ -477,7 +480,6 @@ impl SolanaAccount {
|
||||
}
|
||||
|
||||
/// Decodes one account DTO from the Solana JSON wire shape.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccount>(method, value);
|
||||
let wire = match decoded {
|
||||
@@ -526,7 +528,6 @@ impl SolanaKeyedAccount {
|
||||
}
|
||||
|
||||
/// Decodes one keyed account from the Solana JSON wire shape.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireKeyedAccount>(method, value);
|
||||
let wire = match decoded {
|
||||
@@ -568,7 +569,6 @@ impl SolanaAccountBalance {
|
||||
}
|
||||
|
||||
/// Decodes one account-balance entry from the Solana JSON wire shape.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccountBalance>(method, value);
|
||||
let wire = match decoded {
|
||||
@@ -592,7 +592,361 @@ pub enum SolanaProgramAccountsResult {
|
||||
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
|
||||
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
|
||||
const MAX_MEMCMP_BYTES: usize = 128;
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getAccountInfo` through the common KSP HTTP transport path.
|
||||
pub async fn get_account_info(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
account: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::option::Option<crate::SolanaAccount>>> {
|
||||
let method_result = account_descriptor("getAccountInfo");
|
||||
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(account.to_string())];
|
||||
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_account_info_response("getAccountInfo", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getLargestAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_largest_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
config: std::option::Option<&crate::SolanaLargestAccountsConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaAccountBalance>>> {
|
||||
let method_result = account_descriptor("getLargestAccounts");
|
||||
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::Vec::new();
|
||||
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_largest_accounts_response("getLargestAccounts", value);
|
||||
}
|
||||
|
||||
/// Executes typed `getMinimumBalanceForRentExemption` through the common KSP HTTP transport path.
|
||||
pub async fn get_minimum_balance_for_rent_exemption(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
data_len: usize,
|
||||
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||
) -> ksp_core_lib::Result<u64> {
|
||||
let method_result = account_descriptor("getMinimumBalanceForRentExemption");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let data_len_value = serde_json::to_value(data_len);
|
||||
let data_len_value = match data_len_value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_ENCODE_FAILED, "rent-exemption data length could not be encoded")
|
||||
.with_context("rpc_method", "getMinimumBalanceForRentExemption")
|
||||
.with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut params = std::vec![data_len_value];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment().is_some()
|
||||
{
|
||||
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),
|
||||
};
|
||||
let decoded = crate::decode_wire_json::<u64>("getMinimumBalanceForRentExemption", value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes typed `getMultipleAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_multiple_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
accounts: &[ksp_core_lib::Pubkey],
|
||||
config: std::option::Option<&crate::SolanaAccountInfoConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<std::option::Option<crate::SolanaAccount>>>> {
|
||||
if accounts.len() > MAX_MULTIPLE_ACCOUNTS {
|
||||
return invalid_account_parameters("getMultipleAccounts", "getMultipleAccounts accepts at most 100 public keys", "account_count", accounts.len());
|
||||
}
|
||||
let method_result = account_descriptor("getMultipleAccounts");
|
||||
let method = match method_result {
|
||||
std::result::Result::Ok(method) => method,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let addresses = accounts.iter().map(std::string::ToString::to_string).map(serde_json::Value::String).collect::<std::vec::Vec<_>>();
|
||||
let mut params = std::vec![serde_json::Value::Array(addresses)];
|
||||
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_multiple_accounts_response("getMultipleAccounts", value, accounts.len());
|
||||
}
|
||||
|
||||
/// Executes typed `getProgramAccounts` through the common KSP HTTP transport path.
|
||||
pub async fn get_program_accounts(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
program_id: &ksp_core_lib::Pubkey,
|
||||
config: std::option::Option<&crate::SolanaProgramAccountsConfig>,
|
||||
) -> ksp_core_lib::Result<crate::SolanaProgramAccountsResult> {
|
||||
if let std::option::Option::Some(config) = config {
|
||||
let validation = validate_program_account_filters(config.filters());
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
let method_result = account_descriptor("getProgramAccounts");
|
||||
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(program_id.to_string())];
|
||||
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_program_accounts_response("getProgramAccounts", value);
|
||||
}
|
||||
}
|
||||
|
||||
fn account_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::Accounts && 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 Accounts descriptor is missing from the audited 0.2.2 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn invalid_account_parameters<T>(method: &str, message: &str, field: &'static str, value: usize) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
|
||||
.with_context("rpc_method", method)
|
||||
.with_context(field, value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_program_account_filters(filters: &[crate::SolanaProgramAccountFilter]) -> ksp_core_lib::Result<()> {
|
||||
if filters.len() > MAX_PROGRAM_ACCOUNT_FILTERS {
|
||||
return invalid_account_parameters(
|
||||
"getProgramAccounts",
|
||||
"getProgramAccounts accepts at most 4 filters on the targeted Agave runtime",
|
||||
"filter_count",
|
||||
filters.len(),
|
||||
);
|
||||
}
|
||||
for filter in filters {
|
||||
if let crate::SolanaProgramAccountFilter::Memcmp(memcmp) = filter
|
||||
&& let crate::SolanaMemcmpBytes::Bytes(bytes) = memcmp.bytes()
|
||||
&& bytes.len() > MAX_MEMCMP_BYTES
|
||||
{
|
||||
return invalid_account_parameters(
|
||||
"getProgramAccounts",
|
||||
"raw getProgramAccounts memcmp data accepts at most 128 bytes",
|
||||
"memcmp_byte_count",
|
||||
bytes.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn decode_account_info_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::option::Option<crate::SolanaAccount>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::option::Option<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 account = match wire.value {
|
||||
std::option::Option::Some(value) => {
|
||||
let account = crate::SolanaAccount::decode_wire(method, value);
|
||||
match account {
|
||||
std::result::Result::Ok(account) => std::option::Option::Some(account),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, account));
|
||||
}
|
||||
|
||||
fn decode_largest_accounts_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaAccountBalance>>> {
|
||||
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 values = std::vec::Vec::with_capacity(wire.value.len());
|
||||
for value in wire.value {
|
||||
let decoded = crate::SolanaAccountBalance::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(decoded) => values.push(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, values));
|
||||
}
|
||||
|
||||
fn decode_multiple_accounts_response(
|
||||
method: &str,
|
||||
value: serde_json::Value,
|
||||
expected_count: usize,
|
||||
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<std::option::Option<crate::SolanaAccount>>>> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcResponse<std::vec::Vec<std::option::Option<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 values = std::vec::Vec::with_capacity(wire.value.len());
|
||||
for value in wire.value {
|
||||
match value {
|
||||
std::option::Option::Some(value) => {
|
||||
let decoded = crate::SolanaAccount::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(decoded) => values.push(std::option::Option::Some(decoded)),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
std::option::Option::None => values.push(std::option::Option::None),
|
||||
}
|
||||
}
|
||||
if values.len() != expected_count {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "getMultipleAccounts result count does not match the requested account count")
|
||||
.with_context("rpc_method", method)
|
||||
.with_context("expected_count", expected_count.to_string())
|
||||
.with_context("actual_count", values.len().to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, values));
|
||||
}
|
||||
|
||||
fn decode_program_accounts_response(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<crate::SolanaProgramAccountsResult> {
|
||||
let decoded = crate::decode_wire_json::<WireProgramAccountsResult>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match wire {
|
||||
WireProgramAccountsResult::Accounts(values) => {
|
||||
let decoded = decode_keyed_accounts(method, values);
|
||||
match decoded {
|
||||
std::result::Result::Ok(values) => std::result::Result::Ok(crate::SolanaProgramAccountsResult::Accounts(values)),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
WireProgramAccountsResult::Context(wire) => {
|
||||
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 values = decode_keyed_accounts(method, wire.value);
|
||||
match values {
|
||||
std::result::Result::Ok(values) => {
|
||||
std::result::Result::Ok(crate::SolanaProgramAccountsResult::Context(crate::SolanaRpcResponse::new(context, values)))
|
||||
},
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_keyed_accounts(method: &str, values: std::vec::Vec<serde_json::Value>) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaKeyedAccount>> {
|
||||
let mut decoded_values = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let decoded = crate::SolanaKeyedAccount::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(decoded) => decoded_values.push(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(decoded_values);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireProgramAccountsResult {
|
||||
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
|
||||
Accounts(std::vec::Vec<serde_json::Value>),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireAccountData {
|
||||
@@ -601,7 +955,6 @@ enum WireAccountData {
|
||||
Encoded((std::string::String, std::string::String)),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireParsedAccountData {
|
||||
program: std::string::String,
|
||||
@@ -609,7 +962,6 @@ struct WireParsedAccountData {
|
||||
space: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccount {
|
||||
lamports: u64,
|
||||
@@ -622,14 +974,12 @@ struct WireAccount {
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireKeyedAccount {
|
||||
pubkey: std::string::String,
|
||||
account: serde_json::Value,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccountBalance {
|
||||
address: std::string::String,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -45,7 +45,6 @@ impl SolanaCommitmentConfig {
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn to_json_value(self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
@@ -149,8 +148,7 @@ impl<T> SolanaRpcResponse<T> {
|
||||
return &self.value;
|
||||
}
|
||||
|
||||
/// Creates a contextual response after wire decoding and validation for staged DTO tests.
|
||||
#[cfg(test)]
|
||||
/// Creates a contextual response after wire decoding and validation.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(context: crate::SolanaRpcContext, value: T) -> Self {
|
||||
return Self { context, value };
|
||||
@@ -171,7 +169,6 @@ pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, val
|
||||
}
|
||||
|
||||
/// Parses a base58 public key from one wire field without echoing its value into diagnostics.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||
let parsed = value.parse::<ksp_core_lib::Pubkey>();
|
||||
return match parsed {
|
||||
|
||||
@@ -198,3 +198,14 @@ fn public_pre_002_shared_rpc_types_are_constructible_from_crate_root() {
|
||||
);
|
||||
assert_eq!(vote.vote_pubkey(), std::option::Option::Some(&pubkey));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_003_account_wrappers_are_available_from_crate_root() {
|
||||
let _get_account_info = ksp_onchain_transport_lib::HttpTransportPool::get_account_info;
|
||||
let _get_largest_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_largest_accounts;
|
||||
let _get_minimum_balance_for_rent_exemption = ksp_onchain_transport_lib::HttpTransportPool::get_minimum_balance_for_rent_exemption;
|
||||
let _get_multiple_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_multiple_accounts;
|
||||
let _get_program_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_program_accounts;
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||
|
||||
@@ -46,3 +46,22 @@ fn release_foundation_canaries_and_historical_statuses_are_exact() {
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_pre_003_accounts_subset_is_exact_without_advancing_other_v0_2_2_families() {
|
||||
let mut account_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::Accounts
|
||||
{
|
||||
account_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);
|
||||
}
|
||||
}
|
||||
account_names.sort_unstable();
|
||||
assert_eq!(
|
||||
account_names,
|
||||
std::vec!["getAccountInfo", "getLargestAccounts", "getMinimumBalanceForRentExemption", "getMultipleAccounts", "getProgramAccounts",],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn account_config_serializes_all_common_fields() {
|
||||
@@ -88,3 +88,359 @@ fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
|
||||
assert_eq!(balance.address().to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(balance.lamports(), 99);
|
||||
}
|
||||
|
||||
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_account_info_serializes_config_and_preserves_account_wire() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_account_info.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaDataSliceConfig::new(2, 4)),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(400_000_000),
|
||||
);
|
||||
let response = pool
|
||||
.get_account_info(&crate::HttpRoleName::new("default"), &account, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("getAccountInfo fixture must succeed");
|
||||
assert_eq!(response.context().slot(), 410_000_001);
|
||||
let returned = response.value().as_ref().expect("fixture account must be present");
|
||||
assert_eq!(returned.lamports(), 2_039_280);
|
||||
assert_eq!(returned.owner(), &account);
|
||||
assert_eq!(returned.space(), std::option::Option::Some(4));
|
||||
assert!(matches!(returned.data(), crate::SolanaAccountData::Encoded { encoding: crate::SolanaAccountEncoding::Base64, .. }));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getAccountInfo"));
|
||||
assert_eq!(body["params"][0], serde_json::json!(account.to_string()));
|
||||
assert_eq!(body["params"][1]["encoding"], serde_json::json!("base64"));
|
||||
assert_eq!(body["params"][1]["dataSlice"], serde_json::json!({"offset":2,"length":4}));
|
||||
assert_eq!(body["params"][1]["commitment"], serde_json::json!("finalized"));
|
||||
assert_eq!(body["params"][1]["minContextSlot"], serde_json::json!(400_000_000_u64));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_account_info_preserves_missing_account_as_none() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_account_info.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let response = pool
|
||||
.get_account_info(&crate::HttpRoleName::new("default"), &account, std::option::Option::None)
|
||||
.await
|
||||
.expect("missing getAccountInfo fixture must succeed");
|
||||
assert_eq!(response.value(), &std::option::Option::None);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"].as_array().map(std::vec::Vec::len), std::option::Option::Some(1));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_account_info_rejects_invalid_owner_without_exposing_value() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_account_info.invalid_owner.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let result = pool.get_account_info(&crate::HttpRoleName::new("default"), &account, std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid account owner must fail typed decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!format!("{error:?}").contains("not-a-pubkey"));
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_largest_accounts_serializes_extended_config_and_decodes_order() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_largest_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaLargestAccountsConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(crate::SolanaLargestAccountsFilter::Circulating),
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
let response = pool
|
||||
.get_largest_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("getLargestAccounts fixture must succeed");
|
||||
assert_eq!(response.value().len(), 2);
|
||||
assert_eq!(response.value()[0].lamports(), 999_999_999);
|
||||
assert_eq!(response.value()[1].lamports(), 888_888_888);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getLargestAccounts"));
|
||||
assert_eq!(body["params"][0], serde_json::json!({"commitment":"finalized","filter":"circulating","sortResults":true}));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_largest_accounts_rejects_invalid_address() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_largest_accounts.invalid_address.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_largest_accounts(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||
let error = result.expect_err("invalid largest-account address must fail typed decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert!(!format!("{error:?}").contains("invalid-address"));
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_minimum_balance_for_rent_exemption_serializes_length_and_commitment() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_minimum_balance_for_rent_exemption.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed));
|
||||
let result = pool
|
||||
.get_minimum_balance_for_rent_exemption(&crate::HttpRoleName::new("default"), 50, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("rent-exemption fixture must succeed");
|
||||
assert_eq!(result, 890_880);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getMinimumBalanceForRentExemption"));
|
||||
assert_eq!(body["params"], serde_json::json!([50,{"commitment":"processed"}]));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_minimum_balance_for_rent_exemption_preserves_rpc_application_error() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_minimum_balance_for_rent_exemption.error.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let result = pool.get_minimum_balance_for_rent_exemption(&crate::HttpRoleName::new("default"), usize::MAX, std::option::Option::None).await;
|
||||
let error = result.expect_err("remote invalid parameter 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_multiple_accounts_preserves_order_nulls_and_config() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_multiple_accounts.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let accounts = std::vec![
|
||||
fixture_pubkey("11111111111111111111111111111111"),
|
||||
fixture_pubkey("ComputeBudget111111111111111111111111111111"),
|
||||
fixture_pubkey("Stake11111111111111111111111111111111111111"),
|
||||
];
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::JsonParsed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||
std::option::Option::Some(400_000_001),
|
||||
);
|
||||
let response = pool
|
||||
.get_multiple_accounts(&crate::HttpRoleName::new("default"), accounts.as_slice(), std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("getMultipleAccounts fixture must succeed");
|
||||
assert_eq!(response.value().len(), 3);
|
||||
assert_eq!(response.value()[0].as_ref().map(crate::SolanaAccount::lamports), std::option::Option::Some(10));
|
||||
assert!(response.value()[1].is_none());
|
||||
assert_eq!(response.value()[2].as_ref().map(crate::SolanaAccount::lamports), std::option::Option::Some(20));
|
||||
assert!(matches!(response.value()[2].as_ref().map(crate::SolanaAccount::data), std::option::Option::Some(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!("getMultipleAccounts"));
|
||||
assert_eq!(body["params"][0], serde_json::json!(accounts.iter().map(std::string::ToString::to_string).collect::<std::vec::Vec<_>>()));
|
||||
assert_eq!(body["params"][1]["encoding"], serde_json::json!("jsonParsed"));
|
||||
assert_eq!(body["params"][1]["commitment"], serde_json::json!("confirmed"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_multiple_accounts_rejects_more_than_100_before_io() {
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let account = fixture_pubkey("11111111111111111111111111111111");
|
||||
let accounts = std::vec![account; 101];
|
||||
let result = pool.get_multiple_accounts(&crate::HttpRoleName::new("default"), accounts.as_slice(), std::option::Option::None).await;
|
||||
let error = result.expect_err("more than 100 accounts must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[0].key(), "rpc_method");
|
||||
assert_eq!(error.context()[1].key(), "account_count");
|
||||
assert_eq!(error.context()[1].value(), "101");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_program_accounts_serializes_filters_and_decodes_bare_result() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_program_accounts.bare.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let program_id = fixture_pubkey("11111111111111111111111111111111");
|
||||
let filters = std::vec![
|
||||
crate::SolanaProgramAccountFilter::DataSize(3),
|
||||
crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Base64("AQID".to_owned()))),
|
||||
crate::SolanaProgramAccountFilter::TokenAccountState,
|
||||
];
|
||||
let account_config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaDataSliceConfig::new(0, 3)),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(400_000_002),
|
||||
);
|
||||
let config = crate::SolanaProgramAccountsConfig::new(account_config, filters, std::option::Option::Some(false), std::option::Option::Some(true));
|
||||
let result = pool
|
||||
.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("bare getProgramAccounts fixture must succeed");
|
||||
assert!(matches!(&result, crate::SolanaProgramAccountsResult::Accounts(_)), "bare fixture must preserve the bare account-list result");
|
||||
if let crate::SolanaProgramAccountsResult::Accounts(accounts) = result {
|
||||
assert_eq!(accounts.len(), 1);
|
||||
assert_eq!(accounts[0].account().lamports(), 42);
|
||||
}
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getProgramAccounts"));
|
||||
assert_eq!(
|
||||
body["params"][1]["filters"],
|
||||
serde_json::json!([{"dataSize":3},{"memcmp":{"offset":0,"bytes":"AQID","encoding":"base64"}},"tokenAccountState"]),
|
||||
);
|
||||
assert_eq!(body["params"][1]["withContext"], serde_json::json!(false));
|
||||
assert_eq!(body["params"][1]["sortResults"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_program_accounts_preserves_contextual_result() {
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_program_accounts.context.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let program_id = fixture_pubkey("11111111111111111111111111111111");
|
||||
let config = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec::Vec::new(),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool
|
||||
.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&config))
|
||||
.await
|
||||
.expect("contextual getProgramAccounts fixture must succeed");
|
||||
assert!(matches!(&result, crate::SolanaProgramAccountsResult::Context(_)), "contextual fixture must preserve RpcResponse wrapper");
|
||||
if let crate::SolanaProgramAccountsResult::Context(response) = result {
|
||||
assert_eq!(response.context().slot(), 410_000_007);
|
||||
assert_eq!(response.value().len(), 1);
|
||||
}
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"][1]["withContext"], serde_json::json!(true));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn typed_get_program_accounts_rejects_filter_cardinality_and_oversized_raw_memcmp_before_io() {
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let program_id = fixture_pubkey("11111111111111111111111111111111");
|
||||
let too_many = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec![
|
||||
crate::SolanaProgramAccountFilter::DataSize(1),
|
||||
crate::SolanaProgramAccountFilter::DataSize(2),
|
||||
crate::SolanaProgramAccountFilter::DataSize(3),
|
||||
crate::SolanaProgramAccountFilter::DataSize(4),
|
||||
crate::SolanaProgramAccountFilter::DataSize(5),
|
||||
],
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&too_many)).await;
|
||||
let error = result.expect_err("more than four program-account filters must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].value(), "5");
|
||||
|
||||
let oversized = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec![crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Bytes(std::vec![0_u8; 129]),))],
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let result = pool.get_program_accounts(&crate::HttpRoleName::new("default"), &program_id, std::option::Option::Some(&oversized)).await;
|
||||
let error = result.expect_err("raw memcmp data above 128 bytes must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].value(), "129");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_get_multiple_accounts_rejects_response_count_mismatch() {
|
||||
let value = serde_json::json!({
|
||||
"context":{"apiVersion":"4.2.1","slot":410000008},
|
||||
"value":[]
|
||||
});
|
||||
let result = super::decode_multiple_accounts_response("getMultipleAccounts", value, 1);
|
||||
let error = result.expect_err("response count mismatch must fail typed decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||
assert_eq!(error.context()[1].key(), "expected_count");
|
||||
assert_eq!(error.context()[1].value(), "1");
|
||||
assert_eq!(error.context()[2].key(), "actual_count");
|
||||
assert_eq!(error.context()[2].value(), "0");
|
||||
}
|
||||
|
||||
335
deltas/0.2.2/pre.003.md
Normal file
335
deltas/0.2.2/pre.003.md
Normal file
@@ -0,0 +1,335 @@
|
||||
<!-- file: deltas/0.2.2/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.003` — cinq wrappers HTTP Accounts typés
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente corrigée et validée localement par l'opérateur :
|
||||
|
||||
```text
|
||||
0.2.2-pre.002-fix.002
|
||||
workspace.package.version = "0.2.2-pre.2.fix.2"
|
||||
```
|
||||
|
||||
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
|
||||
85 unit tests
|
||||
9 public API tests
|
||||
2 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 Accounts attribuée à `0.2.2` avec cinq wrappers publics typés :
|
||||
|
||||
```text
|
||||
getAccountInfo
|
||||
getLargestAccounts
|
||||
getMinimumBalanceForRentExemption
|
||||
getMultipleAccounts
|
||||
getProgramAccounts
|
||||
```
|
||||
|
||||
Tous passent par la foundation HTTP `0.2.1` :
|
||||
|
||||
```text
|
||||
wrapper typé
|
||||
-> descriptor audité
|
||||
-> execute_standard_rpc
|
||||
-> pool/admission/retry/deadline
|
||||
-> reqwest HTTP
|
||||
-> JSON-RPC validation
|
||||
-> décodage DTO KSP
|
||||
```
|
||||
|
||||
Aucun wrapper ne contacte `reqwest` directement.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément à `VER-ID-009` :
|
||||
|
||||
```text
|
||||
0.2.2-pre.2.fix.2 -> 0.2.2-pre.3
|
||||
```
|
||||
|
||||
Aucune dépendance ni feature Cargo n'est ajoutée.
|
||||
|
||||
## Surface typée Accounts
|
||||
|
||||
### `getAccountInfo`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + Pubkey + Option<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Option<SolanaAccount>>
|
||||
```
|
||||
|
||||
Le wrapper :
|
||||
|
||||
- sérialise `encoding`, `dataSlice`, `commitment` et `minContextSlot` ;
|
||||
- omet l'objet config lorsqu'il est vide ;
|
||||
- conserve l'absence du compte sous `None` ;
|
||||
- conserve les formes Account data legacy, tuple encodé et `jsonParsed` ;
|
||||
- convertit l'owner en `ksp_core_lib::Pubkey` sans recopier une valeur invalide dans le diagnostic.
|
||||
|
||||
### `getLargestAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + Option<SolanaLargestAccountsConfig>
|
||||
-> SolanaRpcResponse<Vec<SolanaAccountBalance>>
|
||||
```
|
||||
|
||||
Le wrapper conserve l'ordre du wire et sérialise les champs KSP déjà audités dans `pre.002`, y compris `filter` et `sortResults` lorsqu'ils sont
|
||||
explicitement renseignés.
|
||||
|
||||
### `getMinimumBalanceForRentExemption`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + usize data_len + Option<SolanaCommitmentConfig>
|
||||
-> u64 lamports
|
||||
```
|
||||
|
||||
Le wrapper encode la longueur de données sans valeur sentinelle et conserve les erreurs JSON-RPC applicatives comme telles.
|
||||
|
||||
### `getMultipleAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + &[Pubkey] + Option<SolanaAccountInfoConfig>
|
||||
-> SolanaRpcResponse<Vec<Option<SolanaAccount>>>
|
||||
```
|
||||
|
||||
Le wrapper :
|
||||
|
||||
- refuse localement plus de 100 public keys ;
|
||||
- sérialise les adresses dans l'ordre fourni ;
|
||||
- conserve les `null` individuels ;
|
||||
- vérifie que le nombre d'éléments retournés correspond au nombre d'adresses demandées, afin de ne pas fabriquer silencieusement un mapping
|
||||
positionnel incohérent.
|
||||
|
||||
### `getProgramAccounts`
|
||||
|
||||
Signature publique conceptuelle :
|
||||
|
||||
```text
|
||||
role + program Pubkey + Option<SolanaProgramAccountsConfig>
|
||||
-> SolanaProgramAccountsResult
|
||||
```
|
||||
|
||||
Le résultat préserve les deux formes wire :
|
||||
|
||||
```text
|
||||
Accounts(Vec<SolanaKeyedAccount>)
|
||||
Context(SolanaRpcResponse<Vec<SolanaKeyedAccount>>)
|
||||
```
|
||||
|
||||
Le wrapper sérialise `dataSize`, `memcmp`, `tokenAccountState`, `withContext`, `sortResults` et la config Account commune.
|
||||
|
||||
## Validation locale des paramètres
|
||||
|
||||
Nouveau code d'erreur public Transport :
|
||||
|
||||
```text
|
||||
ERROR_CODE_INVALID_RPC_PARAMETERS
|
||||
onchain_transport.invalid_rpc_parameters
|
||||
```
|
||||
|
||||
Il distingue une requête KSP localement invalide d'une erreur JSON-RPC distante ou d'une réponse invalide.
|
||||
|
||||
Les invariants appliqués avant I/O sont :
|
||||
|
||||
```text
|
||||
getMultipleAccounts : <= 100 public keys
|
||||
getProgramAccounts : <= 4 filters
|
||||
memcmp raw bytes : <= 128 bytes
|
||||
```
|
||||
|
||||
Pour `memcmp` encodé sous forme base58/base64, KSP ne décode toujours pas localement la chaîne : la limite des 128 octets décodés reste donc
|
||||
contrôlée par le serveur. Cette décision évite d'ajouter `bs58` ou `base64` uniquement pour une validation anticipée.
|
||||
|
||||
## Wire Account activé en production
|
||||
|
||||
Les helpers privés préparés par `pre.002` et réellement nécessaires à ces wrappers deviennent production-live :
|
||||
|
||||
- sérialisation des configs Account/Largest/Program ;
|
||||
- conversion des encodings Account ;
|
||||
- décodage `SolanaAccountData` ;
|
||||
- décodage Account/KeyedAccount/AccountBalance ;
|
||||
- conversion `Pubkey` wire ;
|
||||
- construction interne de `SolanaRpcResponse<T>`.
|
||||
|
||||
Les helpers Cluster/Token préparatoires restent inchangés et ne deviennent pas production-live avant leurs tranches respectives.
|
||||
|
||||
## Fixtures HTTP déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.null.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.invalid_owner.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.invalid_address.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_multiple_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.bare.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.context.success.json
|
||||
```
|
||||
|
||||
Les tests locaux utilisent un serveur HTTP loopback et n'accèdent pas à Internet.
|
||||
|
||||
## Couverture de tests ajoutée
|
||||
|
||||
Les tests Accounts couvrent :
|
||||
|
||||
- request exacte pour chaque méthode ;
|
||||
- config et omission de config vide ;
|
||||
- succès typé ;
|
||||
- account absent ;
|
||||
- `null` dans `getMultipleAccounts` ;
|
||||
- ordre de `getMultipleAccounts` ;
|
||||
- résultat bare et contextualisé de `getProgramAccounts` ;
|
||||
- invalid owner/address ;
|
||||
- erreur JSON-RPC applicative ;
|
||||
- limite des 100 comptes ;
|
||||
- limite des 4 filtres ;
|
||||
- limite raw `memcmp` de 128 octets ;
|
||||
- mismatch cardinalité request/result de `getMultipleAccounts`.
|
||||
|
||||
Le test public compile explicitement les cinq méthodes sur `HttpTransportPool` et vérifie le nouveau code d'erreur.
|
||||
|
||||
Un canari release supplémentaire fige l'ensemble Accounts `0.2.2` exact :
|
||||
|
||||
```text
|
||||
getAccountInfo
|
||||
getLargestAccounts
|
||||
getMinimumBalanceForRentExemption
|
||||
getMultipleAccounts
|
||||
getProgramAccounts
|
||||
```
|
||||
|
||||
Il vérifie aussi que ces descriptors restent `Read + RetrySafe`. Aucune famille Tokens/Cluster n'est déclarée typed-complete par cette tranche.
|
||||
|
||||
Après application, la cible Transport attendue devient :
|
||||
|
||||
```text
|
||||
98 unit tests
|
||||
10 public API tests
|
||||
3 release completeness tests
|
||||
```
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.null.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_account_info.invalid_owner.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_largest_accounts.invalid_address.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_minimum_balance_for_rent_exemption.error.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_multiple_accounts.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.bare.success.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/get_program_accounts.context.success.json
|
||||
deltas/0.2.2/pre.003.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/error.rs
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/executor.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
La tranche suivante reste `pre.004 — cinq wrappers Tokens`.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- prise en compte de la validation opérateur propre de `pre.002-fix.002` comme base ;
|
||||
- relecture des règles `VERSION_WORKFLOW.md` et `RULES_RUST.md` applicables ;
|
||||
- revérification de la documentation HTTP Solana actuelle pour les cinq méthodes Accounts ;
|
||||
- recoupement des signatures/limites avec la source Agave `v4.2.1` auditée par le plan ;
|
||||
- validation JSON syntaxique des dix nouvelles fixtures ;
|
||||
- contrôle statique de l'absence de `unwrap`, `expect`, `panic!` et `#[allow(dead_code)]` dans le code de production modifié ;
|
||||
- contrôle statique que les cinq wrappers utilisent `execute_standard_rpc` et aucun appel `reqwest` direct ;
|
||||
- contrôle statique de l'absence de nouvelle dépendance/feature ;
|
||||
- contrôle exact des fichiers inclus dans l'archive d'échange.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Le sandbox d'échange ne fournit pas `cargo`, `rustc` ou `rustfmt`. Les commandes suivantes doivent être exécutées sur le checkout opérateur avant
|
||||
commit :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
`cargo test --workspace` reste réservé au checkpoint de clôture/session conformément au workflow KSP. Aucun `cargo tree` n'est requis par cette
|
||||
tranche puisqu'aucune dépendance ni feature n'a changé.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- utiliser les DTOs `pre.002` sans créer de seconde couche RPC ;
|
||||
- conserver `execute_standard_rpc()` comme unique entrée d'exécution HTTP standard ;
|
||||
- introduire un code d'erreur dédié aux paramètres typed invalides plutôt que détourner `invalid_settings` ;
|
||||
- vérifier localement les limites dont KSP peut connaître la violation sans dépendance de décodage supplémentaire ;
|
||||
- préserver les `null`, l'ordre et la dualité bare/context au lieu de normaliser artificiellement les réponses ;
|
||||
- ne pas modifier Config ;
|
||||
- ne pas ajouter de logging spécifique par méthode.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.004`.
|
||||
|
||||
## Suite
|
||||
|
||||
`0.2.2-pre.004` : implémenter les cinq wrappers Tokens (`getTokenAccountBalance`, `getTokenAccountsByDelegate`, `getTokenAccountsByOwner`,
|
||||
`getTokenLargestAccounts`, `getTokenSupply`) avec les DTOs préparés, selector `Mint | ProgramId`, fixtures HTTP déterministes et conservation de
|
||||
`uiAmount: null`.
|
||||
Reference in New Issue
Block a user