995 lines
39 KiB
Rust
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;
|