// file: crates/ksp-onchain-transport-lib/src/rpc_tokens.rs // version: 3 /// Exclusive selector accepted by token-account list RPC methods. #[derive(Clone, Debug, Eq, PartialEq)] pub enum SolanaTokenAccountSelector { /// Select token accounts for one mint. Mint(ksp_core_lib::Pubkey), /// Select token accounts owned by one token program. ProgramId(ksp_core_lib::Pubkey), } impl SolanaTokenAccountSelector { /// Serializes the exclusive selector to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(&self) -> serde_json::Value { return match self { Self::Mint(pubkey) => serde_json::json!({"mint": pubkey.to_string()}), Self::ProgramId(pubkey) => serde_json::json!({"programId": pubkey.to_string()}), }; } } /// Token amount returned by Solana HTTP token RPC methods. #[derive(Clone, Debug, PartialEq)] pub struct SolanaTokenAmount { amount: std::string::String, decimals: u8, ui_amount: std::option::Option, ui_amount_string: std::string::String, } impl SolanaTokenAmount { /// Returns the integer token amount as an exact decimal string. #[must_use] pub fn amount(&self) -> &str { return self.amount.as_str(); } /// Returns the mint decimal precision. #[must_use] pub const fn decimals(&self) -> u8 { return self.decimals; } /// Returns the nullable floating-point UI amount exactly as provided by RPC. #[must_use] pub const fn ui_amount(&self) -> std::option::Option { return self.ui_amount; } /// Returns the exact UI amount string provided by RPC. #[must_use] pub fn ui_amount_string(&self) -> &str { return self.ui_amount_string.as_str(); } /// Decodes one token amount 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); return match decoded { std::result::Result::Ok(wire) => std::result::Result::Ok(Self { amount: wire.amount, decimals: wire.decimals, ui_amount: wire.ui_amount, ui_amount_string: wire.ui_amount_string, }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Token-account balance entry returned by `getTokenLargestAccounts`. #[derive(Clone, Debug, PartialEq)] pub struct SolanaTokenAccountBalance { address: ksp_core_lib::Pubkey, amount: crate::SolanaTokenAmount, } impl SolanaTokenAccountBalance { /// Returns the token account address. #[must_use] pub const fn address(&self) -> &ksp_core_lib::Pubkey { return &self.address; } /// Returns the token amount fields. #[must_use] pub const fn amount(&self) -> &crate::SolanaTokenAmount { return &self.amount; } /// Decodes one token-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()); let address = match address { std::result::Result::Ok(address) => address, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let amount = crate::SolanaTokenAmount { amount: wire.amount, decimals: wire.decimals, ui_amount: wire.ui_amount, ui_amount_string: wire.ui_amount_string, }; return std::result::Result::Ok(Self { address, amount }); } } #[derive(serde::Deserialize)] struct WireTokenAmount { amount: std::string::String, decimals: u8, #[serde(rename = "uiAmount")] ui_amount: std::option::Option, #[serde(rename = "uiAmountString")] ui_amount_string: std::string::String, } #[derive(serde::Deserialize)] struct WireTokenAccountBalance { address: std::string::String, amount: std::string::String, decimals: u8, #[serde(rename = "uiAmount")] ui_amount: std::option::Option, #[serde(rename = "uiAmountString")] ui_amount_string: std::string::String, } #[derive(serde::Deserialize)] struct WireRpcResponse { context: serde_json::Value, value: T, } impl crate::HttpTransportPool { /// Executes typed `getTokenAccountBalance` through the common KSP HTTP transport path. pub async fn get_token_account_balance( &self, role: &crate::HttpRoleName, token_account: &ksp_core_lib::Pubkey, config: std::option::Option<&crate::SolanaCommitmentConfig>, ) -> ksp_core_lib::Result> { let method_result = token_descriptor("getTokenAccountBalance"); 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(token_account.to_string())]; push_commitment_config(&mut params, config); 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_token_amount_response("getTokenAccountBalance", value); } /// Executes typed `getTokenAccountsByDelegate` through the common KSP HTTP transport path. pub async fn get_token_accounts_by_delegate( &self, role: &crate::HttpRoleName, delegate: &ksp_core_lib::Pubkey, selector: &crate::SolanaTokenAccountSelector, config: std::option::Option<&crate::SolanaAccountInfoConfig>, ) -> ksp_core_lib::Result>> { return self.get_token_accounts_list("getTokenAccountsByDelegate", role, delegate, selector, config).await; } /// Executes typed `getTokenAccountsByOwner` through the common KSP HTTP transport path. pub async fn get_token_accounts_by_owner( &self, role: &crate::HttpRoleName, owner: &ksp_core_lib::Pubkey, selector: &crate::SolanaTokenAccountSelector, config: std::option::Option<&crate::SolanaAccountInfoConfig>, ) -> ksp_core_lib::Result>> { return self.get_token_accounts_list("getTokenAccountsByOwner", role, owner, selector, config).await; } /// Executes typed `getTokenLargestAccounts` through the common KSP HTTP transport path. pub async fn get_token_largest_accounts( &self, role: &crate::HttpRoleName, mint: &ksp_core_lib::Pubkey, config: std::option::Option<&crate::SolanaCommitmentConfig>, ) -> ksp_core_lib::Result>> { let method_result = token_descriptor("getTokenLargestAccounts"); 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(mint.to_string())]; push_commitment_config(&mut params, config); 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_token_account_balances_response("getTokenLargestAccounts", value); } /// Executes typed `getTokenSupply` through the common KSP HTTP transport path. pub async fn get_token_supply( &self, role: &crate::HttpRoleName, mint: &ksp_core_lib::Pubkey, config: std::option::Option<&crate::SolanaCommitmentConfig>, ) -> ksp_core_lib::Result> { let method_result = token_descriptor("getTokenSupply"); 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(mint.to_string())]; push_commitment_config(&mut params, config); 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_token_amount_response("getTokenSupply", value); } async fn get_token_accounts_list( &self, method_name: &'static str, role: &crate::HttpRoleName, address: &ksp_core_lib::Pubkey, selector: &crate::SolanaTokenAccountSelector, config: std::option::Option<&crate::SolanaAccountInfoConfig>, ) -> ksp_core_lib::Result>> { let method_result = token_descriptor(method_name); 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(address.to_string()), selector.to_json_value()]; 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_keyed_accounts_response(method_name, value); } } fn token_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::Tokens && 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 Tokens descriptor is missing from the audited 0.2.2 registry") .with_context("rpc_method", method), ), }; } fn push_commitment_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaCommitmentConfig>) { if let std::option::Option::Some(config) = config && config.commitment().is_some() { params.push(config.to_json_value()); } return; } fn decode_token_amount_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 amount = crate::SolanaTokenAmount::decode_wire(method, wire.value); let amount = match amount { std::result::Result::Ok(amount) => amount, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, amount)); } fn decode_keyed_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 accounts = std::vec::Vec::with_capacity(wire.value.len()); for value in wire.value { let account = crate::SolanaKeyedAccount::decode_wire(method, value); match account { std::result::Result::Ok(account) => accounts.push(account), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, accounts)); } fn decode_token_account_balances_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 accounts = std::vec::Vec::with_capacity(wire.value.len()); for value in wire.value { let account = crate::SolanaTokenAccountBalance::decode_wire(method, value); match account { std::result::Result::Ok(account) => accounts.push(account), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, accounts)); } #[cfg(test)] #[path = "../unit_tests/rpc_tokens.rs"] mod tests;