diff --git a/account-decoder/src/lib.rs b/account-decoder/src/lib.rs index 8b7db31f4f..69ab1383a1 100644 --- a/account-decoder/src/lib.rs +++ b/account-decoder/src/lib.rs @@ -8,10 +8,12 @@ pub mod parse_nonce; pub mod parse_token; pub mod parse_vote; -use crate::parse_account_data::{parse_account_data, ParsedAccount}; +use crate::parse_account_data::{parse_account_data, AccountAdditionalData, ParsedAccount}; use solana_sdk::{account::Account, clock::Epoch, pubkey::Pubkey}; use std::str::FromStr; +pub type StringAmount = String; + /// A duplicate representation of an Account for pretty JSON serialization #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] @@ -44,11 +46,17 @@ pub enum UiAccountEncoding { } impl UiAccount { - pub fn encode(account: Account, encoding: UiAccountEncoding) -> Self { + pub fn encode( + account: Account, + encoding: UiAccountEncoding, + additional_data: Option, + ) -> Self { let data = match encoding { UiAccountEncoding::Binary => account.data.into(), UiAccountEncoding::JsonParsed => { - if let Ok(parsed_data) = parse_account_data(&account.owner, &account.data) { + if let Ok(parsed_data) = + parse_account_data(&account.owner, &account.data, additional_data) + { UiAccountData::Json(parsed_data) } else { account.data.into() diff --git a/account-decoder/src/parse_account_data.rs b/account-decoder/src/parse_account_data.rs index 213ec55238..a196726238 100644 --- a/account-decoder/src/parse_account_data.rs +++ b/account-decoder/src/parse_account_data.rs @@ -30,6 +30,9 @@ pub enum ParseAccountError { #[error("Program not parsable")] ProgramNotParsable, + #[error("Additional data required to parse: {0}")] + AdditionalDataMissing(String), + #[error("Instruction error")] InstructionError(#[from] InstructionError), @@ -52,16 +55,25 @@ pub enum ParsableAccount { Vote, } +#[derive(Default)] +pub struct AccountAdditionalData { + pub spl_token_decimals: Option, +} + pub fn parse_account_data( program_id: &Pubkey, data: &[u8], + additional_data: Option, ) -> Result { let program_name = PARSABLE_PROGRAM_IDS .get(program_id) .ok_or_else(|| ParseAccountError::ProgramNotParsable)?; + let additional_data = additional_data.unwrap_or_default(); let parsed_json = match program_name { ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?, - ParsableAccount::SplToken => serde_json::to_value(parse_token(data)?)?, + ParsableAccount::SplToken => { + serde_json::to_value(parse_token(data, additional_data.spl_token_decimals)?)? + } ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?, }; Ok(ParsedAccount { @@ -83,18 +95,19 @@ mod test { fn test_parse_account_data() { let other_program = Pubkey::new_rand(); let data = vec![0; 4]; - assert!(parse_account_data(&other_program, &data).is_err()); + assert!(parse_account_data(&other_program, &data, None).is_err()); let vote_state = VoteState::default(); let mut vote_account_data: Vec = vec![0; VoteState::size_of()]; let versioned = VoteStateVersions::Current(Box::new(vote_state)); VoteState::serialize(&versioned, &mut vote_account_data).unwrap(); - let parsed = parse_account_data(&solana_vote_program::id(), &vote_account_data).unwrap(); + let parsed = + parse_account_data(&solana_vote_program::id(), &vote_account_data, None).unwrap(); assert_eq!(parsed.program, "vote".to_string()); let nonce_data = Versions::new_current(State::Initialized(Data::default())); let nonce_account_data = bincode::serialize(&nonce_data).unwrap(); - let parsed = parse_account_data(&system_program::id(), &nonce_account_data).unwrap(); + let parsed = parse_account_data(&system_program::id(), &nonce_account_data, None).unwrap(); assert_eq!(parsed.program, "nonce".to_string()); } } diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs index 3c6617a1e1..04201c9d9e 100644 --- a/account-decoder/src/parse_token.rs +++ b/account-decoder/src/parse_token.rs @@ -1,4 +1,7 @@ -use crate::parse_account_data::{ParsableAccount, ParseAccountError}; +use crate::{ + parse_account_data::{ParsableAccount, ParseAccountError}, + StringAmount, +}; use solana_sdk::pubkey::Pubkey; use spl_token_v1_0::{ option::COption, @@ -19,15 +22,24 @@ 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 { +pub fn parse_token( + data: &[u8], + mint_decimals: Option, +) -> Result { let mut data = data.to_vec(); if data.len() == size_of::() { let account: Account = *unpack(&mut data) .map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?; + let decimals = mint_decimals.ok_or_else(|| { + ParseAccountError::AdditionalDataMissing( + "no mint_decimals provided to parse spl-token account".to_string(), + ) + })?; + let ui_token_amount = token_amount_to_ui_amount(account.amount, decimals); Ok(TokenAccountType::Account(UiTokenAccount { mint: account.mint.to_string(), owner: account.owner.to_string(), - amount: account.amount, + token_amount: ui_token_amount, delegate: match account.delegate { COption::Some(pubkey) => Some(pubkey.to_string()), COption::None => None, @@ -86,13 +98,31 @@ pub enum TokenAccountType { pub struct UiTokenAccount { pub mint: String, pub owner: String, - pub amount: u64, + pub token_amount: UiTokenAmount, pub delegate: Option, pub is_initialized: bool, pub is_native: bool, pub delegated_amount: u64, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct UiTokenAmount { + pub ui_amount: f64, + pub decimals: u8, + pub amount: StringAmount, +} + +pub fn token_amount_to_ui_amount(amount: u64, decimals: u8) -> UiTokenAmount { + // 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; + UiTokenAmount { + ui_amount: amount_decimals, + decimals, + amount: amount.to_string(), + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct UiMint { @@ -110,6 +140,14 @@ pub struct UiMultisig { pub signers: Vec, } +pub fn get_token_account_mint(data: &[u8]) -> Option { + if data.len() == size_of::() { + Some(Pubkey::new(&data[0..32])) + } else { + None + } +} + #[cfg(test)] mod test { use super::*; @@ -125,12 +163,17 @@ mod test { account.owner = owner_pubkey; account.amount = 42; account.is_initialized = true; + assert!(parse_token(&account_data, None).is_err()); assert_eq!( - parse_token(&account_data).unwrap(), + parse_token(&account_data, Some(2)).unwrap(), TokenAccountType::Account(UiTokenAccount { mint: mint_pubkey.to_string(), owner: owner_pubkey.to_string(), - amount: 42, + token_amount: UiTokenAmount { + ui_amount: 0.42, + decimals: 2, + amount: "42".to_string() + }, delegate: None, is_initialized: true, is_native: false, @@ -144,7 +187,7 @@ mod test { mint.decimals = 3; mint.is_initialized = true; assert_eq!( - parse_token(&mint_data).unwrap(), + parse_token(&mint_data, None).unwrap(), TokenAccountType::Mint(UiMint { owner: Some(owner_pubkey.to_string()), decimals: 3, @@ -166,7 +209,7 @@ mod test { multisig.is_initialized = true; multisig.signers = signers; assert_eq!( - parse_token(&multisig_data).unwrap(), + parse_token(&multisig_data, None).unwrap(), TokenAccountType::Multisig(UiMultisig { num_required_signers: 2, num_valid_signers: 3, @@ -180,6 +223,20 @@ mod test { ); let bad_data = vec![0; 4]; - assert!(parse_token(&bad_data).is_err()); + assert!(parse_token(&bad_data, None).is_err()); + } + + #[test] + fn test_get_token_account_mint() { + let mint_pubkey = SplTokenPubkey::new(&[2; 32]); + let mut account_data = [0; size_of::()]; + let mut account: &mut Account = unpack_unchecked(&mut account_data).unwrap(); + account.mint = mint_pubkey; + + let expected_mint_pubkey = Pubkey::new(&[2; 32]); + assert_eq!( + get_token_account_mint(&account_data), + Some(expected_mint_pubkey) + ); } } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 1755fd1636..61cb142b41 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1235,7 +1235,7 @@ fn process_show_account( let cli_account = CliAccount { keyed_account: RpcKeyedAccount { pubkey: account_pubkey.to_string(), - account: UiAccount::encode(account, UiAccountEncoding::Binary), + account: UiAccount::encode(account, UiAccountEncoding::Binary, None), }, use_lamports_unit, }; diff --git a/cli/src/offline/blockhash_query.rs b/cli/src/offline/blockhash_query.rs index 09c3924f63..fa8ceb999a 100644 --- a/cli/src/offline/blockhash_query.rs +++ b/cli/src/offline/blockhash_query.rs @@ -350,7 +350,7 @@ mod tests { ) .unwrap(); let nonce_pubkey = Pubkey::new(&[4u8; 32]); - let rpc_nonce_account = UiAccount::encode(nonce_account, UiAccountEncoding::Binary); + let rpc_nonce_account = UiAccount::encode(nonce_account, UiAccountEncoding::Binary, None); let get_account_response = json!(Response { context: RpcResponseContext { slot: 1 }, value: json!(Some(rpc_nonce_account)), diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index fefbbab6af..d5e6e4e188 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -15,7 +15,10 @@ use indicatif::{ProgressBar, ProgressStyle}; use log::*; use serde_json::{json, Value}; use solana_account_decoder::{ - parse_token::{parse_token, TokenAccountType, UiMint, UiMultisig, UiTokenAccount}, + parse_token::{ + get_token_account_mint, parse_token, TokenAccountType, UiMint, UiMultisig, UiTokenAccount, + UiTokenAmount, + }, UiAccount, }; use solana_sdk::{ @@ -38,6 +41,7 @@ use solana_transaction_status::{ }; use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY; use std::{ + collections::HashMap, net::SocketAddr, thread::sleep, time::{Duration, Instant}, @@ -698,14 +702,30 @@ impl RpcClient { value: account, } = self.get_account_with_commitment(pubkey, commitment_config)?; + if account.is_none() { + return Err(RpcError::ForUser(format!("AccountNotFound: pubkey={}", pubkey)).into()); + } + let account = account.unwrap(); + let mint = get_token_account_mint(&account.data) + .and_then(|mint_pubkey| { + self.get_token_mint_with_commitment(&mint_pubkey, commitment_config) + .ok() + .map(|response| response.value) + .flatten() + }) + .ok_or_else(|| { + Into::::into(RpcError::ForUser(format!( + "AccountNotFound: mint for token acccount pubkey={}", + pubkey + ))) + })?; + Ok(Response { context, - value: account - .map(|account| match parse_token(&account.data) { - Ok(TokenAccountType::Account(ui_token_account)) => Some(ui_token_account), - _ => None, - }) - .flatten(), + value: match parse_token(&account.data, Some(mint.decimals)) { + Ok(TokenAccountType::Account(ui_token_account)) => Some(ui_token_account), + _ => None, + }, }) } @@ -728,7 +748,7 @@ impl RpcClient { Ok(Response { context, value: account - .map(|account| match parse_token(&account.data) { + .map(|account| match parse_token(&account.data, None) { Ok(TokenAccountType::Mint(ui_token_mint)) => Some(ui_token_mint), _ => None, }) @@ -755,7 +775,7 @@ impl RpcClient { Ok(Response { context, value: account - .map(|account| match parse_token(&account.data) { + .map(|account| match parse_token(&account.data, None) { Ok(TokenAccountType::Multisig(ui_token_multisig)) => Some(ui_token_multisig), _ => None, }) @@ -763,7 +783,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) @@ -773,7 +793,7 @@ impl RpcClient { &self, pubkey: &Pubkey, commitment_config: CommitmentConfig, - ) -> RpcResult { + ) -> RpcResult { self.send( RpcRequest::GetTokenAccountBalance, json!([pubkey.to_string(), commitment_config]), @@ -817,10 +837,10 @@ impl RpcClient { commitment_config ]), )?; - let pubkey_accounts = accounts_to_token_accounts(parse_keyed_accounts( - accounts, - RpcRequest::GetTokenAccountsByDelegate, - )?); + let pubkey_accounts = self.accounts_to_token_accounts( + commitment_config, + parse_keyed_accounts(accounts, RpcRequest::GetTokenAccountsByDelegate)?, + ); Ok(Response { context, value: pubkey_accounts, @@ -860,17 +880,17 @@ impl RpcClient { RpcRequest::GetTokenAccountsByOwner, json!([owner.to_string(), token_account_filter, commitment_config]), )?; - let pubkey_accounts = accounts_to_token_accounts(parse_keyed_accounts( - accounts, - RpcRequest::GetTokenAccountsByDelegate, - )?); + let pubkey_accounts = self.accounts_to_token_accounts( + commitment_config, + parse_keyed_accounts(accounts, RpcRequest::GetTokenAccountsByDelegate)?, + ); Ok(Response { context, value: pubkey_accounts, }) } - 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) @@ -880,13 +900,42 @@ impl RpcClient { &self, mint: &Pubkey, commitment_config: CommitmentConfig, - ) -> RpcResult { + ) -> RpcResult { self.send( RpcRequest::GetTokenSupply, json!([mint.to_string(), commitment_config]), ) } + fn accounts_to_token_accounts( + &self, + commitment_config: CommitmentConfig, + pubkey_accounts: Vec<(Pubkey, Account)>, + ) -> Vec<(Pubkey, UiTokenAccount)> { + let mut mint_decimals: HashMap = HashMap::new(); + pubkey_accounts + .into_iter() + .filter_map(|(pubkey, account)| { + let mint_pubkey = get_token_account_mint(&account.data)?; + let decimals = mint_decimals.get(&mint_pubkey).cloned().or_else(|| { + let mint = self + .get_token_mint_with_commitment(&mint_pubkey, commitment_config) + .ok() + .map(|response| response.value) + .flatten()?; + mint_decimals.insert(mint_pubkey, mint.decimals); + Some(mint.decimals) + })?; + match parse_token(&account.data, Some(decimals)) { + Ok(TokenAccountType::Account(ui_token_account)) => { + Some((pubkey, ui_token_account)) + } + _ => None, + } + }) + .collect() + } + fn poll_balance_with_timeout_and_commitment( &self, pubkey: &Pubkey, @@ -1251,18 +1300,6 @@ fn parse_keyed_accounts( Ok(pubkey_accounts) } -fn accounts_to_token_accounts( - pubkey_accounts: Vec<(Pubkey, Account)>, -) -> Vec<(Pubkey, UiTokenAccount)> { - pubkey_accounts - .into_iter() - .filter_map(|(pubkey, account)| match parse_token(&account.data) { - Ok(TokenAccountType::Account(ui_token_account)) => Some((pubkey, ui_token_account)), - _ => None, - }) - .collect() -} - #[cfg(test)] mod tests { use super::*; diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index f90447b409..2d70853cda 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -1,5 +1,5 @@ use crate::client_error; -use solana_account_decoder::UiAccount; +use solana_account_decoder::{parse_token::UiTokenAmount, UiAccount}; use solana_sdk::{ clock::{Epoch, Slot}, fee_calculator::{FeeCalculator, FeeRateGovernor}, @@ -10,7 +10,6 @@ use solana_transaction_status::ConfirmedTransactionStatusWithSignature; 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 { @@ -222,20 +221,12 @@ 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, #[serde(flatten)] - pub amount: RpcTokenAmount, + pub amount: UiTokenAmount, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 61b813ebaa..7b0f43016f 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -9,7 +9,11 @@ use bincode::{config::Options, serialize}; use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_derive::rpc; use solana_account_decoder::{ - parse_token::{spl_token_id_v1_0, spl_token_v1_0_native_mint}, + parse_account_data::AccountAdditionalData, + parse_token::{ + get_token_account_mint, spl_token_id_v1_0, spl_token_v1_0_native_mint, + token_amount_to_ui_amount, UiTokenAmount, + }, UiAccount, UiAccountEncoding, }; use solana_client::{ @@ -242,8 +246,14 @@ impl JsonRpcRequestProcessor { let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); new_response( &bank, - bank.get_account(pubkey) - .map(|account| UiAccount::encode(account, encoding)), + bank.get_account(pubkey).and_then(|account| { + if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed + { + get_parsed_token_account(bank.clone(), account) + } else { + Some(UiAccount::encode(account, encoding, None)) + } + }), ) } @@ -265,12 +275,17 @@ impl JsonRpcRequestProcessor { let config = config.unwrap_or_default(); let bank = self.bank(config.commitment); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); - get_filtered_program_accounts(&bank, program_id, filters) - .map(|(pubkey, account)| RpcKeyedAccount { - pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone()), - }) - .collect() + let keyed_accounts = get_filtered_program_accounts(&bank, program_id, filters); + if program_id == &spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed { + get_parsed_token_accounts(bank, keyed_accounts).collect() + } else { + keyed_accounts + .map(|(pubkey, account)| RpcKeyedAccount { + pubkey: pubkey.to_string(), + account: UiAccount::encode(account, encoding.clone(), None), + }) + .collect() + } } pub fn get_inflation_governor( @@ -971,7 +986,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()) @@ -998,7 +1013,7 @@ impl JsonRpcRequestProcessor { &self, mint: &Pubkey, commitment: Option, - ) -> Result> { + ) -> Result> { let bank = self.bank(commitment); let (mint_owner, decimals) = get_mint_owner_and_decimals(&bank, mint)?; if mint_owner != spl_token_id_v1_0() { @@ -1106,12 +1121,17 @@ impl JsonRpcRequestProcessor { encoding: None, })); } - let accounts = get_filtered_program_accounts(&bank, &token_program_id, filters) - .map(|(pubkey, account)| RpcKeyedAccount { - pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone()), - }) - .collect(); + let keyed_accounts = get_filtered_program_accounts(&bank, &token_program_id, filters); + let accounts = if encoding == UiAccountEncoding::JsonParsed { + get_parsed_token_accounts(bank.clone(), keyed_accounts).collect() + } else { + keyed_accounts + .map(|(pubkey, account)| RpcKeyedAccount { + pubkey: pubkey.to_string(), + account: UiAccount::encode(account, encoding.clone(), None), + }) + .collect() + }; Ok(new_response(&bank, accounts)) } @@ -1152,12 +1172,17 @@ impl JsonRpcRequestProcessor { encoding: None, })); } - let accounts = get_filtered_program_accounts(&bank, &token_program_id, filters) - .map(|(pubkey, account)| RpcKeyedAccount { - pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone()), - }) - .collect(); + let keyed_accounts = get_filtered_program_accounts(&bank, &token_program_id, filters); + let accounts = if encoding == UiAccountEncoding::JsonParsed { + get_parsed_token_accounts(bank.clone(), keyed_accounts).collect() + } else { + keyed_accounts + .map(|(pubkey, account)| RpcKeyedAccount { + pubkey: pubkey.to_string(), + account: UiAccount::encode(account, encoding.clone(), None), + }) + .collect() + }; Ok(new_response(&bank, accounts)) } } @@ -1211,6 +1236,47 @@ fn get_filtered_program_accounts( }) } +pub(crate) fn get_parsed_token_account(bank: Arc, account: Account) -> Option { + get_token_account_mint(&account.data) + .and_then(|mint_pubkey| get_mint_owner_and_decimals(&bank, &mint_pubkey).ok()) + .map(|(_, decimals)| { + UiAccount::encode( + account, + UiAccountEncoding::JsonParsed, + Some(AccountAdditionalData { + spl_token_decimals: Some(decimals), + }), + ) + }) +} + +pub(crate) fn get_parsed_token_accounts( + bank: Arc, + keyed_accounts: I, +) -> impl Iterator +where + I: Iterator, +{ + let mut mint_decimals: HashMap = HashMap::new(); + keyed_accounts.filter_map(move |(pubkey, account)| { + get_token_account_mint(&account.data).map(|mint_pubkey| { + let spl_token_decimals = mint_decimals.get(&mint_pubkey).cloned().or_else(|| { + let (_, decimals) = get_mint_owner_and_decimals(&bank, &mint_pubkey).ok()?; + mint_decimals.insert(mint_pubkey, decimals); + Some(decimals) + }); + RpcKeyedAccount { + pubkey: pubkey.to_string(), + account: UiAccount::encode( + account, + UiAccountEncoding::JsonParsed, + Some(AccountAdditionalData { spl_token_decimals }), + ), + } + }) + }) +} + /// Analyze a passed Pubkey that may be a Token program id or Mint address to determine the program /// id and optional Mint fn get_token_program_id_and_mint( @@ -1264,16 +1330,6 @@ fn get_mint_decimals(data: &[u8]) -> Result { .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; @@ -1565,7 +1621,7 @@ pub trait RpcSol { meta: Self::Metadata, pubkey_str: String, commitment: Option, - ) -> Result>; + ) -> Result>; #[rpc(meta, name = "getTokenSupply")] fn get_token_supply( @@ -1573,7 +1629,7 @@ pub trait RpcSol { meta: Self::Metadata, mint_str: String, commitment: Option, - ) -> Result>; + ) -> Result>; #[rpc(meta, name = "getTokenLargestAccounts")] fn get_token_largest_accounts( @@ -2226,7 +2282,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 @@ -2240,7 +2296,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) @@ -4614,7 +4670,7 @@ 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: RpcTokenAmount = + let balance: UiTokenAmount = serde_json::from_value(result["result"]["value"].clone()).unwrap(); let error = f64::EPSILON; assert!((balance.ui_amount - 4.2).abs() < error); @@ -4642,7 +4698,7 @@ 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: RpcTokenAmount = + let supply: UiTokenAmount = serde_json::from_value(result["result"]["value"].clone()).unwrap(); let error = f64::EPSILON; assert!((supply.ui_amount - 2.0 * 4.2).abs() < error); @@ -4902,7 +4958,7 @@ pub mod tests { vec![ RpcTokenAccountBalance { address: token_with_different_mint_pubkey.to_string(), - amount: RpcTokenAmount { + amount: UiTokenAmount { ui_amount: 0.42, decimals: 2, amount: "42".to_string(), @@ -4910,7 +4966,7 @@ pub mod tests { }, RpcTokenAccountBalance { address: token_with_smaller_balance.to_string(), - amount: RpcTokenAmount { + amount: UiTokenAmount { ui_amount: 0.1, decimals: 2, amount: "10".to_string(), diff --git a/core/src/rpc_pubsub.rs b/core/src/rpc_pubsub.rs index 7c911f6002..31dcf30fd9 100644 --- a/core/src/rpc_pubsub.rs +++ b/core/src/rpc_pubsub.rs @@ -676,7 +676,8 @@ mod tests { .get_account(&nonce_account.pubkey()) .unwrap() .data; - let expected_data = parse_account_data(&system_program::id(), &expected_data).unwrap(); + let expected_data = + parse_account_data(&system_program::id(), &expected_data, None).unwrap(); let expected = json!({ "jsonrpc": "2.0", "method": "accountNotification", diff --git a/core/src/rpc_subscriptions.rs b/core/src/rpc_subscriptions.rs index afb3f65a8f..35a030c19f 100644 --- a/core/src/rpc_subscriptions.rs +++ b/core/src/rpc_subscriptions.rs @@ -1,5 +1,6 @@ //! The `pubsub` module implements a threaded subscription service on client RPC request +use crate::rpc::{get_parsed_token_account, get_parsed_token_accounts}; use core::hash::Hash; use jsonrpc_core::futures::Future; use jsonrpc_pubsub::{ @@ -7,7 +8,7 @@ use jsonrpc_pubsub::{ SubscriptionId, }; use serde::Serialize; -use solana_account_decoder::{UiAccount, UiAccountEncoding}; +use solana_account_decoder::{parse_token::spl_token_id_v1_0, UiAccount, UiAccountEncoding}; use solana_client::{ rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, rpc_filter::RpcFilterType, @@ -178,7 +179,7 @@ where K: Eq + Hash + Clone + Copy, S: Clone + Serialize, B: Fn(&Bank, &K) -> X, - F: Fn(X, Slot, Option) -> (Box>, Slot), + F: Fn(X, Slot, Option, Option>) -> (Box>, Slot), X: Clone + Serialize + Default, T: Clone, { @@ -202,16 +203,18 @@ where commitment_slots.highest_confirmed_slot } }; - let results = { - let bank_forks = bank_forks.read().unwrap(); - bank_forks - .get(slot) - .map(|desired_bank| bank_method(&desired_bank, hashmap_key)) - .unwrap_or_default() - }; + let bank = bank_forks.read().unwrap().get(slot).cloned(); + let results = bank + .clone() + .map(|desired_bank| bank_method(&desired_bank, hashmap_key)) + .unwrap_or_default(); let mut w_last_notified_slot = last_notified_slot.write().unwrap(); - let (filter_results, result_slot) = - filter_results(results, *w_last_notified_slot, config.as_ref().cloned()); + let (filter_results, result_slot) = filter_results( + results, + *w_last_notified_slot, + config.as_ref().cloned(), + bank, + ); for result in filter_results { notifier.notify( Response { @@ -244,16 +247,24 @@ fn filter_account_result( result: Option<(Account, Slot)>, last_notified_slot: Slot, encoding: Option, + bank: Option>, ) -> (Box>, Slot) { if let Some((account, fork)) = result { // If fork < last_notified_slot this means that we last notified for a fork // and should notify that the account state has been reverted. if fork != last_notified_slot { let encoding = encoding.unwrap_or(UiAccountEncoding::Binary); - return ( - Box::new(iter::once(UiAccount::encode(account, encoding))), - fork, - ); + if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed { + let bank = bank.unwrap(); // If result.is_some(), bank must also be Some + if let Some(ui_account) = get_parsed_token_account(bank, account) { + return (Box::new(iter::once(ui_account)), fork); + } + } else { + return ( + Box::new(iter::once(UiAccount::encode(account, encoding, None))), + fork, + ); + } } } (Box::new(iter::empty()), last_notified_slot) @@ -263,6 +274,7 @@ fn filter_signature_result( result: Option>, last_notified_slot: Slot, _config: Option<()>, + _bank: Option>, ) -> (Box>, Slot) { ( Box::new( @@ -278,27 +290,30 @@ fn filter_program_results( accounts: Vec<(Pubkey, Account)>, last_notified_slot: Slot, config: Option, + bank: Option>, ) -> (Box>, Slot) { let config = config.unwrap_or_default(); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); let filters = config.filters; - ( - Box::new( - accounts - .into_iter() - .filter(move |(_, account)| { - filters.iter().all(|filter_type| match filter_type { - RpcFilterType::DataSize(size) => account.data.len() as u64 == *size, - RpcFilterType::Memcmp(compare) => compare.bytes_match(&account.data), - }) - }) - .map(move |(pubkey, account)| RpcKeyedAccount { + let keyed_accounts = accounts.into_iter().filter(move |(_, account)| { + filters.iter().all(|filter_type| match filter_type { + RpcFilterType::DataSize(size) => account.data.len() as u64 == *size, + RpcFilterType::Memcmp(compare) => compare.bytes_match(&account.data), + }) + }); + let accounts: Box> = + if encoding == UiAccountEncoding::JsonParsed { + let bank = bank.unwrap(); // If !accounts.is_empty(), bank must be Some + Box::new(get_parsed_token_accounts(bank, keyed_accounts)) + } else { + Box::new( + keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(account, encoding.clone()), + account: UiAccount::encode(account, encoding.clone(), None), }), - ), - last_notified_slot, - ) + ) + }; + (accounts, last_notified_slot) } #[derive(Clone)] @@ -860,7 +875,7 @@ impl RpcSubscriptions { &subscriptions.gossip_account_subscriptions, &subscriptions.gossip_program_subscriptions, &subscriptions.gossip_signature_subscriptions, - &bank_forks, + bank_forks, &commitment_slots, ¬ifier, ); @@ -881,7 +896,7 @@ impl RpcSubscriptions { for pubkey in &pubkeys { Self::check_account( pubkey, - &bank_forks, + bank_forks, account_subscriptions.clone(), ¬ifier, &commitment_slots, @@ -895,7 +910,7 @@ impl RpcSubscriptions { for program_id in &programs { Self::check_program( program_id, - &bank_forks, + bank_forks, program_subscriptions.clone(), ¬ifier, &commitment_slots, @@ -909,7 +924,7 @@ impl RpcSubscriptions { for signature in &signatures { Self::check_signature( signature, - &bank_forks, + bank_forks, signature_subscriptions.clone(), ¬ifier, &commitment_slots, diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 9e73bb46bd..249c98e2f2 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -1118,7 +1118,7 @@ The result will be an RpcResponse JSON object with `value` equal to an array of // Request curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getTokenAccountsByDelegate", "params": ["4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T", {"programId": "TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"}, {"encoding": "jsonParsed"}]}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":[{"data":{"program":"spl-token","parsed":{"accountType":"account","info":{"amount":1,"delegate":"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T","delegatedAmount":1,"isInitialized":true,"isNative":false,"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E","owner":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}}},"executable":false,"lamports":1726080,"owner":"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","rentEpoch":4},"pubkey":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}],"id":1} +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":[{"data":{"program":"spl-token","parsed":{"accountType":"account","info":{"tokenAmount":{"amount":"1","uiAmount":0.1,"decimals":1},"delegate":"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T","delegatedAmount":1,"isInitialized":true,"isNative":false,"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E","owner":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}}},"executable":false,"lamports":1726080,"owner":"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","rentEpoch":4},"pubkey":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}],"id":1} ``` ### getTokenAccountsByOwner @@ -1154,7 +1154,7 @@ The result will be an RpcResponse JSON object with `value` equal to an array of // Request curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getTokenAccountsByOwner", "params": ["4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F", {"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E"}, {"encoding": "jsonParsed"}]}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":[{"data":{"program":"spl-token","parsed":{"accountType":"account","info":{"amount":1,"delegate":null,"delegatedAmount":1,"isInitialized":true,"isNative":false,"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E","owner":"4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F"}}},"executable":false,"lamports":1726080,"owner":"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","rentEpoch":4},"pubkey":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}],"id":1} +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":[{"data":{"program":"spl-token","parsed":{"accountType":"account","info":{"tokenAmount":{"amount":"1","uiAmount":0.1,"decimals":1},"delegate":null,"delegatedAmount":1,"isInitialized":true,"isNative":false,"mint":"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E","owner":"4Qkev8aNZcqFNSRhQzwyLMFSsi94jHqE8WNVTJzTP99F"}}},"executable":false,"lamports":1726080,"owner":"TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o","rentEpoch":4},"pubkey":"CnPoSPKXu7wJqxe59Fs72tkBeALovhsCxYeFwPCQH9TD"}],"id":1} ``` ### getTokenSupply @@ -1481,8 +1481,10 @@ Subscribe to an account to receive notifications when the lamports or data for a }, "value": { "data": { - "nonce": { - "initialized": { + "program": "nonce" + "parsed": { + "type": "initialized", + "info": { "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", "feeCalculator": { @@ -1597,8 +1599,10 @@ Subscribe to a program to receive notifications when the lamports or data for a "pubkey": "H4vnBqifaSACnKa7acsxstsY1iV1bvJNxsCY7enrd1hq" "account": { "data": { - "nonce": { - "initialized": { + "program": "nonce" + "parsed": { + "type": "initialized", + "info": { "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", "feeCalculator": {