Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
2026-08-22 23:08:47 +02:00

995 lines
39 KiB
Rust

// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
// version: 7
const MAX_MEMCMP_BYTES: usize = 128;
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
/// Account-data encoding accepted by Solana account HTTP and WebSocket methods.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SolanaAccountEncoding {
/// Legacy binary/base58 request encoding.
Binary,
/// Base58 text encoding.
Base58,
/// Base64 text encoding.
Base64,
/// Parsed JSON representation when the RPC node has a parser for the account owner.
JsonParsed,
/// Base64 text containing zstd-compressed bytes.
Base64Zstd,
}
impl SolanaAccountEncoding {
/// Returns the Solana JSON-RPC encoding string.
#[must_use]
pub const fn as_str(self) -> &'static str {
return match self {
Self::Binary => "binary",
Self::Base58 => "base58",
Self::Base64 => "base64",
Self::JsonParsed => "jsonParsed",
Self::Base64Zstd => "base64+zstd",
};
}
fn from_wire(value: &str) -> std::option::Option<Self> {
return match value {
"binary" => std::option::Option::Some(Self::Binary),
"base58" => std::option::Option::Some(Self::Base58),
"base64" => std::option::Option::Some(Self::Base64),
"jsonParsed" => std::option::Option::Some(Self::JsonParsed),
"base64+zstd" => std::option::Option::Some(Self::Base64Zstd),
_ => std::option::Option::None,
};
}
}
/// Byte range requested from account data without decoding it locally.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SolanaDataSliceConfig {
offset: usize,
length: usize,
}
impl SolanaDataSliceConfig {
/// Creates an account-data slice configuration.
#[must_use]
pub const fn new(offset: usize, length: usize) -> Self {
return Self { offset, length };
}
/// Returns the byte offset.
#[must_use]
pub const fn offset(&self) -> usize {
return self.offset;
}
/// Returns the requested byte length.
#[must_use]
pub const fn length(&self) -> usize {
return self.length;
}
/// Serializes this data-slice configuration to the Solana JSON-RPC wire object.
pub(crate) fn to_json_value(self) -> serde_json::Value {
return serde_json::json!({"offset": self.offset, "length": self.length});
}
}
/// Shared account configuration used by account-info and token-account list methods.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SolanaAccountInfoConfig {
encoding: std::option::Option<crate::SolanaAccountEncoding>,
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
context: crate::SolanaContextConfig,
}
impl SolanaAccountInfoConfig {
/// Creates an explicit account-info configuration.
#[must_use]
pub const fn new(
encoding: std::option::Option<crate::SolanaAccountEncoding>,
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
commitment: std::option::Option<crate::SolanaCommitment>,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { encoding, data_slice, context: crate::SolanaContextConfig::new(commitment, min_context_slot) };
}
/// Returns the optional account-data encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaAccountEncoding> {
return self.encoding;
}
/// Returns the optional account-data slice.
#[must_use]
pub const fn data_slice(&self) -> std::option::Option<crate::SolanaDataSliceConfig> {
return self.data_slice;
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.context.commitment();
}
/// Returns the optional minimum context slot.
#[must_use]
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
return self.context.min_context_slot();
}
/// Returns whether empty.
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();
}
/// Serializes this config to the Solana JSON-RPC wire object.
#[must_use]
pub(crate) fn to_json_value(&self) -> serde_json::Value {
let context_value = self.context.to_json_value();
let mut object = match context_value {
serde_json::Value::Object(object) => object,
_ => serde_json::Map::new(),
};
if let std::option::Option::Some(encoding) = self.encoding {
object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned()));
}
if let std::option::Option::Some(data_slice) = self.data_slice {
object.insert("dataSlice".to_owned(), data_slice.to_json_value());
}
return serde_json::Value::Object(object);
}
}
/// Filter accepted by `getLargestAccounts`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SolanaLargestAccountsFilter {
/// Return only circulating accounts.
Circulating,
/// Return only non-circulating accounts.
NonCirculating,
}
impl SolanaLargestAccountsFilter {
fn as_str(self) -> &'static str {
return match self {
Self::Circulating => "circulating",
Self::NonCirculating => "nonCirculating",
};
}
}
/// Optional configuration for `getLargestAccounts`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SolanaLargestAccountsConfig {
commitment: std::option::Option<crate::SolanaCommitment>,
filter: std::option::Option<crate::SolanaLargestAccountsFilter>,
sort_results: std::option::Option<bool>,
}
impl SolanaLargestAccountsConfig {
/// Creates a largest-accounts configuration.
#[must_use]
pub const fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
filter: std::option::Option<crate::SolanaLargestAccountsFilter>,
sort_results: std::option::Option<bool>,
) -> Self {
return Self { commitment, filter, sort_results };
}
/// Returns the optional commitment.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional circulating-account filter.
#[must_use]
pub const fn filter(&self) -> std::option::Option<crate::SolanaLargestAccountsFilter> {
return self.filter;
}
/// Returns the optional server-side result-sorting request.
#[must_use]
pub const fn sort_results(&self) -> std::option::Option<bool> {
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]
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 {
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
}
if let std::option::Option::Some(filter) = self.filter {
object.insert("filter".to_owned(), serde_json::Value::String(filter.as_str().to_owned()));
}
if let std::option::Option::Some(sort_results) = self.sort_results {
object.insert("sortResults".to_owned(), serde_json::Value::Bool(sort_results));
}
return serde_json::Value::Object(object);
}
}
/// Bytes used by a `memcmp` program-account filter.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SolanaMemcmpBytes {
/// Base58-encoded bytes.
Base58(std::string::String),
/// Base64-encoded bytes.
Base64(std::string::String),
/// Raw byte array.
Bytes(std::vec::Vec<u8>),
}
/// One `memcmp` filter applied to account data.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolanaMemcmpFilter {
offset: usize,
bytes: crate::SolanaMemcmpBytes,
}
impl SolanaMemcmpFilter {
/// Creates a `memcmp` filter without locally decoding encoded string data.
#[must_use]
pub fn new(offset: usize, bytes: crate::SolanaMemcmpBytes) -> Self {
return Self { offset, bytes };
}
/// Returns the account-data byte offset.
#[must_use]
pub const fn offset(&self) -> usize {
return self.offset;
}
/// Returns the encoded or raw bytes.
#[must_use]
pub const fn bytes(&self) -> &crate::SolanaMemcmpBytes {
return &self.bytes;
}
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"}),
crate::SolanaMemcmpBytes::Base64(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base64"}),
crate::SolanaMemcmpBytes::Bytes(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "bytes"}),
};
}
}
/// Filter accepted by the current `getProgramAccounts` implementation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SolanaProgramAccountFilter {
/// Require an exact account data size.
DataSize(u64),
/// Compare bytes at one account-data offset.
Memcmp(crate::SolanaMemcmpFilter),
/// Require a valid SPL Token account-state layout according to the RPC implementation.
TokenAccountState,
}
impl SolanaProgramAccountFilter {
/// Serializes this program-account filter to the Solana JSON-RPC wire representation.
pub(crate) fn to_json_value(&self) -> serde_json::Value {
return match self {
Self::DataSize(size) => serde_json::json!({"dataSize": size}),
Self::Memcmp(filter) => serde_json::json!({"memcmp": filter.to_json_value()}),
Self::TokenAccountState => serde_json::Value::String("tokenAccountState".to_owned()),
};
}
}
/// Configuration for `getProgramAccounts` built from the shared account config plus program filters.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SolanaProgramAccountsConfig {
account_config: crate::SolanaAccountInfoConfig,
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
with_context: std::option::Option<bool>,
sort_results: std::option::Option<bool>,
}
impl SolanaProgramAccountsConfig {
/// Creates a program-accounts configuration.
#[must_use]
pub fn new(
account_config: crate::SolanaAccountInfoConfig,
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
with_context: std::option::Option<bool>,
sort_results: std::option::Option<bool>,
) -> Self {
return Self { account_config, filters, with_context, sort_results };
}
/// Returns the shared account configuration.
#[must_use]
pub const fn account_config(&self) -> &crate::SolanaAccountInfoConfig {
return &self.account_config;
}
/// Returns the ordered program-account filters.
#[must_use]
pub fn filters(&self) -> &[crate::SolanaProgramAccountFilter] {
return self.filters.as_slice();
}
/// Returns the optional context-wrapper request.
#[must_use]
pub const fn with_context(&self) -> std::option::Option<bool> {
return self.with_context;
}
/// Returns the optional server-side sorting request.
#[must_use]
pub const fn sort_results(&self) -> std::option::Option<bool> {
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]
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 {
serde_json::Value::Object(object) => object,
_ => serde_json::Map::new(),
};
if !self.filters.is_empty() {
let values = self.filters.iter().map(crate::SolanaProgramAccountFilter::to_json_value).collect::<std::vec::Vec<_>>();
object.insert("filters".to_owned(), serde_json::Value::Array(values));
}
if let std::option::Option::Some(with_context) = self.with_context {
object.insert("withContext".to_owned(), serde_json::Value::Bool(with_context));
}
if let std::option::Option::Some(sort_results) = self.sort_results {
object.insert("sortResults".to_owned(), serde_json::Value::Bool(sort_results));
}
return serde_json::Value::Object(object);
}
}
/// Parsed account payload returned by the RPC node for `jsonParsed` account data.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaParsedAccountData {
program: std::string::String,
parsed: serde_json::Value,
space: u64,
}
impl SolanaParsedAccountData {
/// Returns the parser/program label reported by the RPC node.
#[must_use]
pub fn program(&self) -> &str {
return self.program.as_str();
}
/// Returns the parsed JSON payload without converting it to a Program/SPL domain model.
#[must_use]
pub const fn parsed(&self) -> &serde_json::Value {
return &self.parsed;
}
/// Returns the account-data space reported inside the parsed payload.
#[must_use]
pub const fn space(&self) -> u64 {
return self.space;
}
}
/// Wire-preserving account data returned by Solana account HTTP and WebSocket methods.
#[derive(Clone, Debug, PartialEq)]
pub enum SolanaAccountData {
/// Legacy single-string binary form retained for backwards compatibility.
LegacyBinary(std::string::String),
/// Encoded tuple `[data, encoding]`.
Encoded {
/// Encoded account bytes.
data: std::string::String,
/// Encoding label returned by the RPC node.
encoding: crate::SolanaAccountEncoding,
},
/// Parsed JSON object returned by the RPC node.
JsonParsed(crate::SolanaParsedAccountData),
}
impl SolanaAccountData {
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 {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match wire {
WireAccountData::LegacyBinary(value) => std::result::Result::Ok(Self::LegacyBinary(value)),
WireAccountData::JsonParsed(value) => {
std::result::Result::Ok(Self::JsonParsed(crate::SolanaParsedAccountData { program: value.program, parsed: value.parsed, space: value.space }))
},
WireAccountData::Encoded((data, encoding)) => {
let parsed = crate::SolanaAccountEncoding::from_wire(encoding.as_str());
let encoding = match parsed {
std::option::Option::Some(encoding) => encoding,
std::option::Option::None => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an unknown encoding")
.with_context("rpc_method", method),
);
},
};
if encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::JsonParsed {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an invalid tuple encoding")
.with_context("rpc_method", method),
);
}
std::result::Result::Ok(Self::Encoded { data, encoding })
},
};
}
}
/// Typed transport-level Solana account without Program/SPL decoding.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaAccount {
lamports: u64,
data: crate::SolanaAccountData,
owner: ksp_core_lib::Pubkey,
executable: bool,
rent_epoch: u64,
space: std::option::Option<u64>,
}
impl SolanaAccount {
/// Returns the account balance in lamports.
#[must_use]
pub const fn lamports(&self) -> u64 {
return self.lamports;
}
/// Returns the wire-preserving account data.
#[must_use]
pub const fn data(&self) -> &crate::SolanaAccountData {
return &self.data;
}
/// Returns the account owner program public key.
#[must_use]
pub const fn owner(&self) -> &ksp_core_lib::Pubkey {
return &self.owner;
}
/// Returns whether the account is executable.
#[must_use]
pub const fn executable(&self) -> bool {
return self.executable;
}
/// Returns the rent epoch reported by the RPC node.
#[must_use]
pub const fn rent_epoch(&self) -> u64 {
return self.rent_epoch;
}
/// Returns the optional account data-space field.
#[must_use]
pub const fn space(&self) -> std::option::Option<u64> {
return self.space;
}
/// Decodes one account DTO from the Solana JSON wire shape.
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 {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = crate::parse_wire_pubkey(method, "owner", wire.owner.as_str());
let owner = match owner {
std::result::Result::Ok(owner) => owner,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let data = crate::SolanaAccountData::decode_wire(method, wire.data);
let data = match data {
std::result::Result::Ok(data) => data,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self {
lamports: wire.lamports,
data,
owner,
executable: wire.executable,
rent_epoch: wire.rent_epoch,
space: wire.space,
});
}
}
/// One public key plus its account returned by account-list RPC methods.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaKeyedAccount {
pubkey: ksp_core_lib::Pubkey,
account: crate::SolanaAccount,
}
impl SolanaKeyedAccount {
/// Returns the account public key.
#[must_use]
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
/// Returns the account payload.
#[must_use]
pub const fn account(&self) -> &crate::SolanaAccount {
return &self.account;
}
/// Decodes one keyed account from the Solana JSON wire shape.
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 {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let pubkey = crate::parse_wire_pubkey(method, "pubkey", wire.pubkey.as_str());
let pubkey = match pubkey {
std::result::Result::Ok(pubkey) => pubkey,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let account = crate::SolanaAccount::decode_wire(method, wire.account);
let account = match account {
std::result::Result::Ok(account) => account,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self { pubkey, account });
}
}
/// Address and lamport balance returned by `getLargestAccounts`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolanaAccountBalance {
address: ksp_core_lib::Pubkey,
lamports: u64,
}
impl SolanaAccountBalance {
/// Returns the account address.
#[must_use]
pub const fn address(&self) -> &ksp_core_lib::Pubkey {
return &self.address;
}
/// Returns the balance in lamports.
#[must_use]
pub const fn lamports(&self) -> u64 {
return self.lamports;
}
/// Decodes one account-balance entry from the Solana JSON wire shape.
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 {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let address = crate::parse_wire_pubkey(method, "address", wire.address.as_str());
return match address {
std::result::Result::Ok(address) => std::result::Result::Ok(Self { address, lamports: wire.lamports }),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Result union returned by `getProgramAccounts` with or without an RPC context.
#[derive(Clone, Debug, PartialEq)]
pub enum SolanaProgramAccountsResult {
/// Bare account list returned when `withContext` is false or absent.
Accounts(std::vec::Vec<crate::SolanaKeyedAccount>),
/// Contextual account list returned when `withContext` is true.
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
}
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);
}
}
#[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 {
LegacyBinary(std::string::String),
JsonParsed(WireParsedAccountData),
Encoded((std::string::String, std::string::String)),
}
#[derive(serde::Deserialize)]
struct WireParsedAccountData {
program: std::string::String,
parsed: serde_json::Value,
space: u64,
}
#[derive(serde::Deserialize)]
struct WireAccount {
lamports: u64,
data: serde_json::Value,
owner: std::string::String,
executable: bool,
#[serde(rename = "rentEpoch")]
rent_epoch: u64,
#[serde(default)]
space: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
struct WireKeyedAccount {
pubkey: std::string::String,
account: serde_json::Value,
}
#[derive(serde::Deserialize)]
struct WireAccountBalance {
address: std::string::String,
lamports: u64,
}
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);
}
#[cfg(test)]
#[path = "../unit_tests/rpc_accounts.rs"]
mod tests;