355 lines
15 KiB
Rust
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;
|