// 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 { 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, data_slice: std::option::Option, context: crate::SolanaContextConfig, } impl SolanaAccountInfoConfig { /// Creates an explicit account-info configuration. #[must_use] pub const fn new( encoding: std::option::Option, data_slice: std::option::Option, commitment: std::option::Option, min_context_slot: std::option::Option, ) -> 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 { return self.encoding; } /// Returns the optional account-data slice. #[must_use] pub const fn data_slice(&self) -> std::option::Option { return self.data_slice; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.context.commitment(); } /// Returns the optional minimum context slot. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { 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, filter: std::option::Option, sort_results: std::option::Option, } impl SolanaLargestAccountsConfig { /// Creates a largest-accounts configuration. #[must_use] pub const fn new( commitment: std::option::Option, filter: std::option::Option, sort_results: std::option::Option, ) -> Self { return Self { commitment, filter, sort_results }; } /// Returns the optional commitment. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional circulating-account filter. #[must_use] pub const fn filter(&self) -> std::option::Option { return self.filter; } /// Returns the optional server-side result-sorting request. #[must_use] pub const fn sort_results(&self) -> std::option::Option { 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), } /// 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, with_context: std::option::Option, sort_results: std::option::Option, } impl SolanaProgramAccountsConfig { /// Creates a program-accounts configuration. #[must_use] pub fn new( account_config: crate::SolanaAccountInfoConfig, filters: std::vec::Vec, with_context: std::option::Option, sort_results: std::option::Option, ) -> 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 { return self.with_context; } /// Returns the optional server-side sorting request. #[must_use] pub const fn sort_results(&self) -> std::option::Option { 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::>(); 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 { let decoded = crate::decode_wire_json::(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, } 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 { 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 { let decoded = crate::decode_wire_json::(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 { let decoded = crate::decode_wire_json::(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 { let decoded = crate::decode_wire_json::(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), /// Contextual account list returned when `withContext` is true. Context(crate::SolanaRpcResponse>), } 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>> { 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>> { 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 { 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::("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>>> { 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::>(); 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 { 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 { context: serde_json::Value, value: T, } #[derive(serde::Deserialize)] #[serde(untagged)] enum WireProgramAccountsResult { Context(WireRpcResponse>), Accounts(std::vec::Vec), } #[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, } #[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(method: &str, message: &str, field: &'static str, value: usize) -> ksp_core_lib::Result { 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>> { let decoded = crate::decode_wire_json::>>(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>> { let decoded = crate::decode_wire_json::>>(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>>> { let decoded = crate::decode_wire_json::>>>(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 { let decoded = crate::decode_wire_json::(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) -> ksp_core_lib::Result> { 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;