Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
2026-08-18 08:09:20 +02:00

355 lines
15 KiB
Rust

// 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<f64>,
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<f64> {
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<Self> {
let decoded = crate::decode_wire_json::<WireTokenAmount>(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<Self> {
let decoded = crate::decode_wire_json::<WireTokenAccountBalance>(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<f64>,
#[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<f64>,
#[serde(rename = "uiAmountString")]
ui_amount_string: std::string::String,
}
#[derive(serde::Deserialize)]
struct WireRpcResponse<T> {
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<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaTokenAccountBalance>>> {
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<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
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<serde_json::Value>, 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<crate::SolanaRpcResponse<crate::SolanaTokenAmount>> {
let decoded = crate::decode_wire_json::<WireRpcResponse<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 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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>> {
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 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<crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaTokenAccountBalance>>> {
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 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;