diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs index 6d39cb94e47ddd..3c6617a1e1a10e 100644 --- a/account-decoder/src/parse_token.rs +++ b/account-decoder/src/parse_token.rs @@ -7,11 +7,18 @@ use spl_token_v1_0::{ }; use std::{mem::size_of, str::FromStr}; -// A helper function to convert spl_token_v1_0::id() as spl_sdk::pubkey::Pubkey to solana_sdk::pubkey::Pubkey +// A helper function to convert spl_token_v1_0::id() as spl_sdk::pubkey::Pubkey to +// solana_sdk::pubkey::Pubkey pub fn spl_token_id_v1_0() -> Pubkey { Pubkey::from_str(&spl_token_v1_0::id().to_string()).unwrap() } +// A helper function to convert spl_token_v1_0::native_mint::id() as spl_sdk::pubkey::Pubkey to +// solana_sdk::pubkey::Pubkey +pub fn spl_token_v1_0_native_mint() -> Pubkey { + Pubkey::from_str(&spl_token_v1_0::native_mint::id().to_string()).unwrap() +} + pub fn parse_token(data: &[u8]) -> Result { let mut data = data.to_vec(); if data.len() == size_of::() { diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 6af82b8a71c582..f95aec000a35bb 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -742,7 +742,7 @@ impl RpcClient { }) } - pub fn get_token_account_balance(&self, pubkey: &Pubkey) -> ClientResult { + pub fn get_token_account_balance(&self, pubkey: &Pubkey) -> ClientResult { Ok(self .get_token_account_balance_with_commitment(pubkey, CommitmentConfig::default())? .value) @@ -752,7 +752,7 @@ impl RpcClient { &self, pubkey: &Pubkey, commitment_config: CommitmentConfig, - ) -> RpcResult { + ) -> RpcResult { self.send( RpcRequest::GetTokenAccountBalance, json!([pubkey.to_string(), commitment_config]), @@ -849,7 +849,7 @@ impl RpcClient { }) } - pub fn get_token_supply(&self, mint: &Pubkey) -> ClientResult { + pub fn get_token_supply(&self, mint: &Pubkey) -> ClientResult { Ok(self .get_token_supply_with_commitment(mint, CommitmentConfig::default())? .value) @@ -859,7 +859,7 @@ impl RpcClient { &self, mint: &Pubkey, commitment_config: CommitmentConfig, - ) -> RpcResult { + ) -> RpcResult { self.send( RpcRequest::GetTokenSupply, json!([mint.to_string(), commitment_config]), diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 4f1133382e3fac..37d390e93d214a 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -9,6 +9,7 @@ use solana_sdk::{ use std::{collections::HashMap, net::SocketAddr}; pub type RpcResult = client_error::Result>; +pub type RpcAmount = String; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RpcResponseContext { @@ -220,9 +221,18 @@ pub struct RpcStakeActivation { pub inactive: u64, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RpcTokenAmount { + pub ui_amount: f64, + pub decimals: u8, + pub amount: RpcAmount, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RpcTokenAccountBalance { pub address: String, - pub amount: u64, + #[serde(flatten)] + pub amount: RpcTokenAmount, } diff --git a/core/src/rpc.rs b/core/src/rpc.rs index cd9ed11125a887..b73949e85c853c 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -13,7 +13,10 @@ use crate::{ use bincode::serialize; use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; -use solana_account_decoder::{parse_token::spl_token_id_v1_0, UiAccount, UiAccountEncoding}; +use solana_account_decoder::{ + parse_token::{spl_token_id_v1_0, spl_token_v1_0_native_mint}, + UiAccount, UiAccountEncoding, +}; use solana_client::{ rpc_config::*, rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, @@ -51,7 +54,7 @@ use solana_transaction_status::{ ConfirmedBlock, ConfirmedTransaction, TransactionStatus, UiTransactionEncoding, }; use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; -use spl_token_v1_0::state::Account as TokenAccount; +use spl_token_v1_0::state::{Account as TokenAccount, Mint}; use std::{ cmp::{max, min}, collections::{HashMap, HashSet}, @@ -788,7 +791,7 @@ impl JsonRpcRequestProcessor { &self, pubkey: &Pubkey, commitment: Option, - ) -> Result> { + ) -> Result> { let bank = self.bank(commitment)?; let account = bank.get_account(pubkey).ok_or_else(|| { Error::invalid_params("Invalid param: could not find account".to_string()) @@ -800,11 +803,14 @@ impl JsonRpcRequestProcessor { )); } let mut data = account.data.to_vec(); - let balance = spl_token_v1_0::state::unpack(&mut data) - .map_err(|_| { + let token_account = + spl_token_v1_0::state::unpack::(&mut data).map_err(|_| { Error::invalid_params("Invalid param: not a v1.0 Token account".to_string()) - }) - .map(|account: &mut TokenAccount| account.amount)?; + })?; + let mint = &Pubkey::from_str(&token_account.mint.to_string()) + .expect("Token account mint should be convertible to Pubkey"); + let (_, decimals) = get_mint_owner_and_decimals(&bank, &mint)?; + let balance = token_amount_to_ui_amount(token_account.amount, decimals); new_response(&bank, balance) } @@ -812,16 +818,15 @@ impl JsonRpcRequestProcessor { &self, mint: &Pubkey, commitment: Option, - ) -> Result> { + ) -> Result> { let bank = self.bank(commitment)?; - let mint_account = bank.get_account(mint).ok_or_else(|| { - Error::invalid_params("Invalid param: could not find mint".to_string()) - })?; - if mint_account.owner != spl_token_id_v1_0() { + let (mint_owner, decimals) = get_mint_owner_and_decimals(&bank, mint)?; + if mint_owner != spl_token_id_v1_0() { return Err(Error::invalid_params( "Invalid param: not a v1.0 Token mint".to_string(), )); } + let filters = vec![ // Filter on Mint address RpcFilterType::Memcmp(Memcmp { @@ -832,7 +837,7 @@ impl JsonRpcRequestProcessor { // Filter on Token Account state RpcFilterType::DataSize(size_of::() as u64), ]; - let supply = get_filtered_program_accounts(&bank, &mint_account.owner, filters) + let supply = get_filtered_program_accounts(&bank, &mint_owner, filters) .map(|(_pubkey, account)| { let mut data = account.data.to_vec(); spl_token_v1_0::state::unpack(&mut data) @@ -840,6 +845,7 @@ impl JsonRpcRequestProcessor { .unwrap_or(0) }) .sum(); + let supply = token_amount_to_ui_amount(supply, decimals); new_response(&bank, supply) } @@ -849,10 +855,8 @@ impl JsonRpcRequestProcessor { commitment: Option, ) -> Result>> { let bank = self.bank(commitment)?; - let mint_account = bank.get_account(mint).ok_or_else(|| { - Error::invalid_params("Invalid param: could not find mint".to_string()) - })?; - if mint_account.owner != spl_token_id_v1_0() { + let (mint_owner, decimals) = get_mint_owner_and_decimals(&bank, mint)?; + if mint_owner != spl_token_id_v1_0() { return Err(Error::invalid_params( "Invalid param: not a v1.0 Token mint".to_string(), )); @@ -868,19 +872,27 @@ impl JsonRpcRequestProcessor { RpcFilterType::DataSize(size_of::() as u64), ]; let mut token_balances: Vec = - get_filtered_program_accounts(&bank, &mint_account.owner, filters) + get_filtered_program_accounts(&bank, &mint_owner, filters) .map(|(address, account)| { let mut data = account.data.to_vec(); let amount = spl_token_v1_0::state::unpack(&mut data) .map(|account: &mut TokenAccount| account.amount) .unwrap_or(0); + let amount = token_amount_to_ui_amount(amount, decimals); RpcTokenAccountBalance { address: address.to_string(), amount, } }) .collect(); - token_balances.sort_by(|a, b| a.amount.cmp(&b.amount).reverse()); + token_balances.sort_by(|a, b| { + a.amount + .amount + .parse::() + .unwrap() + .cmp(&b.amount.amount.parse::().unwrap()) + .reverse() + }); token_balances.truncate(NUM_LARGEST_ACCOUNTS); new_response(&bank, token_balances) } @@ -1046,15 +1058,13 @@ fn get_token_program_id_and_mint( ) -> Result<(Pubkey, Option)> { match token_account_filter { TokenAccountsFilter::Mint(mint) => { - let mint_account = bank.get_account(&mint).ok_or_else(|| { - Error::invalid_params("Invalid param: could not find mint".to_string()) - })?; - if mint_account.owner != spl_token_id_v1_0() { + let (mint_owner, _) = get_mint_owner_and_decimals(&bank, &mint)?; + if mint_owner != spl_token_id_v1_0() { return Err(Error::invalid_params( "Invalid param: not a v1.0 Token mint".to_string(), )); } - Ok((mint_account.owner, Some(mint))) + Ok((mint_owner, Some(mint))) } TokenAccountsFilter::ProgramId(program_id) => { if program_id == spl_token_id_v1_0() { @@ -1068,6 +1078,41 @@ fn get_token_program_id_and_mint( } } +/// Analyze a mint Pubkey that may be the native_mint and get the mint-account owner (token +/// program_id) and decimals +fn get_mint_owner_and_decimals(bank: &Arc, mint: &Pubkey) -> Result<(Pubkey, u8)> { + if mint == &spl_token_v1_0_native_mint() { + // Uncomment the following once spl_token is bumped to a version that includes native_mint::DECIMALS + // Ok((spl_token_id_v1_0(), spl_token_v1_0::native_mint::DECIMALS)) + Ok((spl_token_id_v1_0(), 9)) + } else { + let mint_account = bank.get_account(mint).ok_or_else(|| { + Error::invalid_params("Invalid param: could not find mint".to_string()) + })?; + let decimals = get_mint_decimals(&mint_account.data)?; + Ok((mint_account.owner, decimals)) + } +} + +fn get_mint_decimals(data: &[u8]) -> Result { + let mut data = data.to_vec(); + spl_token_v1_0::state::unpack(&mut data) + .map_err(|_| { + Error::invalid_params("Invalid param: Token mint could not be unpacked".to_string()) + }) + .map(|mint: &mut Mint| mint.decimals) +} + +fn token_amount_to_ui_amount(amount: u64, decimals: u8) -> RpcTokenAmount { + // Use `amount_to_ui_amount()` once spl_token is bumped to a version that supports it: https://github.com/solana-labs/solana-program-library/pull/211 + let amount_decimals = amount as f64 / 10_usize.pow(decimals as u32) as f64; + RpcTokenAmount { + ui_amount: amount_decimals, + decimals, + amount: amount.to_string(), + } +} + #[rpc] pub trait RpcSol { type Metadata; @@ -1351,7 +1396,7 @@ pub trait RpcSol { meta: Self::Metadata, pubkey_str: String, commitment: Option, - ) -> Result>; + ) -> Result>; #[rpc(meta, name = "getTokenSupply")] fn get_token_supply( @@ -1359,7 +1404,7 @@ pub trait RpcSol { meta: Self::Metadata, mint_str: String, commitment: Option, - ) -> Result>; + ) -> Result>; #[rpc(meta, name = "getTokenLargestAccounts")] fn get_token_largest_accounts( @@ -1982,7 +2027,7 @@ impl RpcSol for RpcSolImpl { meta: Self::Metadata, pubkey_str: String, commitment: Option, - ) -> Result> { + ) -> Result> { debug!( "get_token_account_balance rpc request received: {:?}", pubkey_str @@ -1996,7 +2041,7 @@ impl RpcSol for RpcSolImpl { meta: Self::Metadata, mint_str: String, commitment: Option, - ) -> Result> { + ) -> Result> { debug!("get_token_supply rpc request received: {:?}", mint_str); let mint = verify_pubkey(mint_str)?; meta.get_token_supply(&mint, commitment) @@ -4258,7 +4303,7 @@ pub mod tests { mint, owner, delegate: COption::Some(delegate), - amount: 42, + amount: 420, is_initialized: true, is_native: false, delegated_amount: 30, @@ -4272,6 +4317,23 @@ pub mod tests { let token_account_pubkey = Pubkey::new_rand(); bank.store_account(&token_account_pubkey, &token_account); + // Add the mint + let mut mint_data = [0; size_of::()]; + let mint_state: &mut Mint = + spl_token_v1_0::state::unpack_unchecked(&mut mint_data).unwrap(); + *mint_state = Mint { + owner: COption::Some(owner), + decimals: 2, + is_initialized: true, + }; + let mint_account = Account { + lamports: 111, + data: mint_data.to_vec(), + owner: spl_token_id_v1_0(), + ..Account::default() + }; + bank.store_account(&Pubkey::from_str(&mint.to_string()).unwrap(), &mint_account); + let req = format!( r#"{{"jsonrpc":"2.0","id":1,"method":"getTokenAccountBalance","params":["{}"]}}"#, token_account_pubkey, @@ -4279,8 +4341,12 @@ pub mod tests { let res = io.handle_request_sync(&req, meta.clone()); let result: Value = serde_json::from_str(&res.expect("actual response")) .expect("actual response deserialization"); - let balance: u64 = serde_json::from_value(result["result"]["value"].clone()).unwrap(); - assert_eq!(balance, 42); + let balance: RpcTokenAmount = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + let error = f64::EPSILON; + assert!((balance.ui_amount - 4.2).abs() < error); + assert_eq!(balance.amount, 420.to_string()); + assert_eq!(balance.decimals, 2); // Test non-existent token account let req = format!( @@ -4292,22 +4358,7 @@ pub mod tests { .expect("actual response deserialization"); assert!(result.get("error").is_some()); - // Add the mint, plus another token account to ensure getTokenSupply sums all mint accounts - let mut mint_data = [0; size_of::()]; - let mint_state: &mut Mint = - spl_token_v1_0::state::unpack_unchecked(&mut mint_data).unwrap(); - *mint_state = Mint { - owner: COption::Some(owner), - decimals: 2, - is_initialized: true, - }; - let mint_account = Account { - lamports: 111, - data: mint_data.to_vec(), - owner: spl_token_id_v1_0(), - ..Account::default() - }; - bank.store_account(&Pubkey::from_str(&mint.to_string()).unwrap(), &mint_account); + // Add another token account to ensure getTokenSupply sums all mint accounts let other_token_account_pubkey = Pubkey::new_rand(); bank.store_account(&other_token_account_pubkey, &token_account); @@ -4318,8 +4369,12 @@ pub mod tests { let res = io.handle_request_sync(&req, meta.clone()); let result: Value = serde_json::from_str(&res.expect("actual response")) .expect("actual response deserialization"); - let supply: u64 = serde_json::from_value(result["result"]["value"].clone()).unwrap(); - assert_eq!(supply, 2 * 42); + let supply: RpcTokenAmount = + serde_json::from_value(result["result"]["value"].clone()).unwrap(); + let error = f64::EPSILON; + assert!((supply.ui_amount - 2.0 * 4.2).abs() < error); + assert_eq!(supply.amount, (2 * 420).to_string()); + assert_eq!(supply.decimals, 2); // Test non-existent mint address let req = format!( @@ -4574,11 +4629,19 @@ pub mod tests { vec![ RpcTokenAccountBalance { address: token_with_different_mint_pubkey.to_string(), - amount: 42, + amount: RpcTokenAmount { + ui_amount: 0.42, + decimals: 2, + amount: "42".to_string(), + } }, RpcTokenAccountBalance { address: token_with_smaller_balance.to_string(), - amount: 10, + amount: RpcTokenAmount { + ui_amount: 0.1, + decimals: 2, + amount: "10".to_string(), + } } ] ); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index bffe5b7cc4cfa3..f555fad2e47138 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -1031,7 +1031,11 @@ Returns the token balance of an SPL Token account. #### Results: -- `RpcResponse` - RpcResponse JSON object with `value` field set to the balance +The result will be an RpcResponse JSON object with `value` equal to a JSON object containing: + +- `uiAmount: ` - the balance, using mint-prescribed decimals +- `amount: ` - the raw balance without decimals, a string representation of u64 +- `decimals: ` - number of base 10 digits to the right of the decimal place #### Example: @@ -1039,7 +1043,7 @@ Returns the token balance of an SPL Token account. // Request curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getTokenAccountBalance", "params": ["7fUAJdStEuGbc3sM84cKRL6yYaaSstyLSU4ve5oovLS7"]}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":9864,"id":1} +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":{"uiAmount":98.64,"amount":"9864","decimals":2},"id":1} ``` ### getTokenAccountsByDelegate @@ -1125,7 +1129,11 @@ Returns the total supply of an SPL Token type. #### Results: -- `RpcResponse` - RpcResponse JSON object with `value` field set to the total token supply +The result will be an RpcResponse JSON object with `value` equal to a JSON object containing: + +- `uiAmount: ` - the total token supply, using mint-prescribed decimals +- `amount: ` - the raw total token supply without decimals, a string representation of u64 +- `decimals: ` - number of base 10 digits to the right of the decimal place #### Example: @@ -1133,7 +1141,7 @@ Returns the total supply of an SPL Token type. // Request curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getTokenSupply", "params": ["3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E"]}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":100000,"id":1} +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":{"uiAmount":1000.0,"amount":"100000","decimals":2},"id":1} ``` ### getTransactionCount