v0.2.2-pre.003

This commit is contained in:
2026-08-18 07:31:07 +02:00
parent bec1f2ec08
commit f15448ff6e
19 changed files with 1113 additions and 31 deletions

View File

@@ -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,