Token Accounts: return ui_amount, decimals with decoded account (#11407)

* Return ui_amount, decimals from token client methods

* Return ui_amount, decimals in RPC jsonParsed token accounts

* Fixup docs

* Return ui_amount, decimals in pubsub jsonParsed token accounts

* Remove unnecessary duplicate struct

* StringAmount rename
This commit is contained in:
Tyera Eulberg 2020-08-07 11:37:39 -06:00 committed by GitHub
parent 67fdf593a2
commit b7c2681903
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 326 additions and 144 deletions

View File

@ -8,10 +8,12 @@ pub mod parse_nonce;
pub mod parse_token; pub mod parse_token;
pub mod parse_vote; 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 solana_sdk::{account::Account, clock::Epoch, pubkey::Pubkey};
use std::str::FromStr; use std::str::FromStr;
pub type StringAmount = String;
/// A duplicate representation of an Account for pretty JSON serialization /// A duplicate representation of an Account for pretty JSON serialization
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -44,11 +46,17 @@ pub enum UiAccountEncoding {
} }
impl UiAccount { impl UiAccount {
pub fn encode(account: Account, encoding: UiAccountEncoding) -> Self { pub fn encode(
account: Account,
encoding: UiAccountEncoding,
additional_data: Option<AccountAdditionalData>,
) -> Self {
let data = match encoding { let data = match encoding {
UiAccountEncoding::Binary => account.data.into(), UiAccountEncoding::Binary => account.data.into(),
UiAccountEncoding::JsonParsed => { 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) UiAccountData::Json(parsed_data)
} else { } else {
account.data.into() account.data.into()

View File

@ -30,6 +30,9 @@ pub enum ParseAccountError {
#[error("Program not parsable")] #[error("Program not parsable")]
ProgramNotParsable, ProgramNotParsable,
#[error("Additional data required to parse: {0}")]
AdditionalDataMissing(String),
#[error("Instruction error")] #[error("Instruction error")]
InstructionError(#[from] InstructionError), InstructionError(#[from] InstructionError),
@ -52,16 +55,25 @@ pub enum ParsableAccount {
Vote, Vote,
} }
#[derive(Default)]
pub struct AccountAdditionalData {
pub spl_token_decimals: Option<u8>,
}
pub fn parse_account_data( pub fn parse_account_data(
program_id: &Pubkey, program_id: &Pubkey,
data: &[u8], data: &[u8],
additional_data: Option<AccountAdditionalData>,
) -> Result<ParsedAccount, ParseAccountError> { ) -> Result<ParsedAccount, ParseAccountError> {
let program_name = PARSABLE_PROGRAM_IDS let program_name = PARSABLE_PROGRAM_IDS
.get(program_id) .get(program_id)
.ok_or_else(|| ParseAccountError::ProgramNotParsable)?; .ok_or_else(|| ParseAccountError::ProgramNotParsable)?;
let additional_data = additional_data.unwrap_or_default();
let parsed_json = match program_name { let parsed_json = match program_name {
ParsableAccount::Nonce => serde_json::to_value(parse_nonce(data)?)?, 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)?)?, ParsableAccount::Vote => serde_json::to_value(parse_vote(data)?)?,
}; };
Ok(ParsedAccount { Ok(ParsedAccount {
@ -83,18 +95,19 @@ mod test {
fn test_parse_account_data() { fn test_parse_account_data() {
let other_program = Pubkey::new_rand(); let other_program = Pubkey::new_rand();
let data = vec![0; 4]; 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 vote_state = VoteState::default();
let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()]; let mut vote_account_data: Vec<u8> = vec![0; VoteState::size_of()];
let versioned = VoteStateVersions::Current(Box::new(vote_state)); let versioned = VoteStateVersions::Current(Box::new(vote_state));
VoteState::serialize(&versioned, &mut vote_account_data).unwrap(); 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()); assert_eq!(parsed.program, "vote".to_string());
let nonce_data = Versions::new_current(State::Initialized(Data::default())); let nonce_data = Versions::new_current(State::Initialized(Data::default()));
let nonce_account_data = bincode::serialize(&nonce_data).unwrap(); 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()); assert_eq!(parsed.program, "nonce".to_string());
} }
} }

View File

@ -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 solana_sdk::pubkey::Pubkey;
use spl_token_v1_0::{ use spl_token_v1_0::{
option::COption, 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() Pubkey::from_str(&spl_token_v1_0::native_mint::id().to_string()).unwrap()
} }
pub fn parse_token(data: &[u8]) -> Result<TokenAccountType, ParseAccountError> { pub fn parse_token(
data: &[u8],
mint_decimals: Option<u8>,
) -> Result<TokenAccountType, ParseAccountError> {
let mut data = data.to_vec(); let mut data = data.to_vec();
if data.len() == size_of::<Account>() { if data.len() == size_of::<Account>() {
let account: Account = *unpack(&mut data) let account: Account = *unpack(&mut data)
.map_err(|_| ParseAccountError::AccountNotParsable(ParsableAccount::SplToken))?; .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 { Ok(TokenAccountType::Account(UiTokenAccount {
mint: account.mint.to_string(), mint: account.mint.to_string(),
owner: account.owner.to_string(), owner: account.owner.to_string(),
amount: account.amount, token_amount: ui_token_amount,
delegate: match account.delegate { delegate: match account.delegate {
COption::Some(pubkey) => Some(pubkey.to_string()), COption::Some(pubkey) => Some(pubkey.to_string()),
COption::None => None, COption::None => None,
@ -86,13 +98,31 @@ pub enum TokenAccountType {
pub struct UiTokenAccount { pub struct UiTokenAccount {
pub mint: String, pub mint: String,
pub owner: String, pub owner: String,
pub amount: u64, pub token_amount: UiTokenAmount,
pub delegate: Option<String>, pub delegate: Option<String>,
pub is_initialized: bool, pub is_initialized: bool,
pub is_native: bool, pub is_native: bool,
pub delegated_amount: u64, 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)] #[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UiMint { pub struct UiMint {
@ -110,6 +140,14 @@ pub struct UiMultisig {
pub signers: Vec<String>, pub signers: Vec<String>,
} }
pub fn get_token_account_mint(data: &[u8]) -> Option<Pubkey> {
if data.len() == size_of::<Account>() {
Some(Pubkey::new(&data[0..32]))
} else {
None
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -125,12 +163,17 @@ mod test {
account.owner = owner_pubkey; account.owner = owner_pubkey;
account.amount = 42; account.amount = 42;
account.is_initialized = true; account.is_initialized = true;
assert!(parse_token(&account_data, None).is_err());
assert_eq!( assert_eq!(
parse_token(&account_data).unwrap(), parse_token(&account_data, Some(2)).unwrap(),
TokenAccountType::Account(UiTokenAccount { TokenAccountType::Account(UiTokenAccount {
mint: mint_pubkey.to_string(), mint: mint_pubkey.to_string(),
owner: owner_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, delegate: None,
is_initialized: true, is_initialized: true,
is_native: false, is_native: false,
@ -144,7 +187,7 @@ mod test {
mint.decimals = 3; mint.decimals = 3;
mint.is_initialized = true; mint.is_initialized = true;
assert_eq!( assert_eq!(
parse_token(&mint_data).unwrap(), parse_token(&mint_data, None).unwrap(),
TokenAccountType::Mint(UiMint { TokenAccountType::Mint(UiMint {
owner: Some(owner_pubkey.to_string()), owner: Some(owner_pubkey.to_string()),
decimals: 3, decimals: 3,
@ -166,7 +209,7 @@ mod test {
multisig.is_initialized = true; multisig.is_initialized = true;
multisig.signers = signers; multisig.signers = signers;
assert_eq!( assert_eq!(
parse_token(&multisig_data).unwrap(), parse_token(&multisig_data, None).unwrap(),
TokenAccountType::Multisig(UiMultisig { TokenAccountType::Multisig(UiMultisig {
num_required_signers: 2, num_required_signers: 2,
num_valid_signers: 3, num_valid_signers: 3,
@ -180,6 +223,20 @@ mod test {
); );
let bad_data = vec![0; 4]; 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::<Account>()];
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)
);
} }
} }

View File

@ -1235,7 +1235,7 @@ fn process_show_account(
let cli_account = CliAccount { let cli_account = CliAccount {
keyed_account: RpcKeyedAccount { keyed_account: RpcKeyedAccount {
pubkey: account_pubkey.to_string(), pubkey: account_pubkey.to_string(),
account: UiAccount::encode(account, UiAccountEncoding::Binary), account: UiAccount::encode(account, UiAccountEncoding::Binary, None),
}, },
use_lamports_unit, use_lamports_unit,
}; };

View File

@ -350,7 +350,7 @@ mod tests {
) )
.unwrap(); .unwrap();
let nonce_pubkey = Pubkey::new(&[4u8; 32]); 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 { let get_account_response = json!(Response {
context: RpcResponseContext { slot: 1 }, context: RpcResponseContext { slot: 1 },
value: json!(Some(rpc_nonce_account)), value: json!(Some(rpc_nonce_account)),

View File

@ -15,7 +15,10 @@ use indicatif::{ProgressBar, ProgressStyle};
use log::*; use log::*;
use serde_json::{json, Value}; use serde_json::{json, Value};
use solana_account_decoder::{ 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, UiAccount,
}; };
use solana_sdk::{ use solana_sdk::{
@ -38,6 +41,7 @@ use solana_transaction_status::{
}; };
use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY; use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY;
use std::{ use std::{
collections::HashMap,
net::SocketAddr, net::SocketAddr,
thread::sleep, thread::sleep,
time::{Duration, Instant}, time::{Duration, Instant},
@ -698,14 +702,30 @@ impl RpcClient {
value: account, value: account,
} = self.get_account_with_commitment(pubkey, commitment_config)?; } = 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::<ClientError>::into(RpcError::ForUser(format!(
"AccountNotFound: mint for token acccount pubkey={}",
pubkey
)))
})?;
Ok(Response { Ok(Response {
context, context,
value: account value: match parse_token(&account.data, Some(mint.decimals)) {
.map(|account| match parse_token(&account.data) { Ok(TokenAccountType::Account(ui_token_account)) => Some(ui_token_account),
Ok(TokenAccountType::Account(ui_token_account)) => Some(ui_token_account), _ => None,
_ => None, },
})
.flatten(),
}) })
} }
@ -728,7 +748,7 @@ impl RpcClient {
Ok(Response { Ok(Response {
context, context,
value: account 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), Ok(TokenAccountType::Mint(ui_token_mint)) => Some(ui_token_mint),
_ => None, _ => None,
}) })
@ -755,7 +775,7 @@ impl RpcClient {
Ok(Response { Ok(Response {
context, context,
value: account 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), Ok(TokenAccountType::Multisig(ui_token_multisig)) => Some(ui_token_multisig),
_ => None, _ => None,
}) })
@ -763,7 +783,7 @@ impl RpcClient {
}) })
} }
pub fn get_token_account_balance(&self, pubkey: &Pubkey) -> ClientResult<RpcTokenAmount> { pub fn get_token_account_balance(&self, pubkey: &Pubkey) -> ClientResult<UiTokenAmount> {
Ok(self Ok(self
.get_token_account_balance_with_commitment(pubkey, CommitmentConfig::default())? .get_token_account_balance_with_commitment(pubkey, CommitmentConfig::default())?
.value) .value)
@ -773,7 +793,7 @@ impl RpcClient {
&self, &self,
pubkey: &Pubkey, pubkey: &Pubkey,
commitment_config: CommitmentConfig, commitment_config: CommitmentConfig,
) -> RpcResult<RpcTokenAmount> { ) -> RpcResult<UiTokenAmount> {
self.send( self.send(
RpcRequest::GetTokenAccountBalance, RpcRequest::GetTokenAccountBalance,
json!([pubkey.to_string(), commitment_config]), json!([pubkey.to_string(), commitment_config]),
@ -817,10 +837,10 @@ impl RpcClient {
commitment_config commitment_config
]), ]),
)?; )?;
let pubkey_accounts = accounts_to_token_accounts(parse_keyed_accounts( let pubkey_accounts = self.accounts_to_token_accounts(
accounts, commitment_config,
RpcRequest::GetTokenAccountsByDelegate, parse_keyed_accounts(accounts, RpcRequest::GetTokenAccountsByDelegate)?,
)?); );
Ok(Response { Ok(Response {
context, context,
value: pubkey_accounts, value: pubkey_accounts,
@ -860,17 +880,17 @@ impl RpcClient {
RpcRequest::GetTokenAccountsByOwner, RpcRequest::GetTokenAccountsByOwner,
json!([owner.to_string(), token_account_filter, commitment_config]), json!([owner.to_string(), token_account_filter, commitment_config]),
)?; )?;
let pubkey_accounts = accounts_to_token_accounts(parse_keyed_accounts( let pubkey_accounts = self.accounts_to_token_accounts(
accounts, commitment_config,
RpcRequest::GetTokenAccountsByDelegate, parse_keyed_accounts(accounts, RpcRequest::GetTokenAccountsByDelegate)?,
)?); );
Ok(Response { Ok(Response {
context, context,
value: pubkey_accounts, value: pubkey_accounts,
}) })
} }
pub fn get_token_supply(&self, mint: &Pubkey) -> ClientResult<RpcTokenAmount> { pub fn get_token_supply(&self, mint: &Pubkey) -> ClientResult<UiTokenAmount> {
Ok(self Ok(self
.get_token_supply_with_commitment(mint, CommitmentConfig::default())? .get_token_supply_with_commitment(mint, CommitmentConfig::default())?
.value) .value)
@ -880,13 +900,42 @@ impl RpcClient {
&self, &self,
mint: &Pubkey, mint: &Pubkey,
commitment_config: CommitmentConfig, commitment_config: CommitmentConfig,
) -> RpcResult<RpcTokenAmount> { ) -> RpcResult<UiTokenAmount> {
self.send( self.send(
RpcRequest::GetTokenSupply, RpcRequest::GetTokenSupply,
json!([mint.to_string(), commitment_config]), 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<Pubkey, u8> = 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( fn poll_balance_with_timeout_and_commitment(
&self, &self,
pubkey: &Pubkey, pubkey: &Pubkey,
@ -1251,18 +1300,6 @@ fn parse_keyed_accounts(
Ok(pubkey_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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,5 +1,5 @@
use crate::client_error; use crate::client_error;
use solana_account_decoder::UiAccount; use solana_account_decoder::{parse_token::UiTokenAmount, UiAccount};
use solana_sdk::{ use solana_sdk::{
clock::{Epoch, Slot}, clock::{Epoch, Slot},
fee_calculator::{FeeCalculator, FeeRateGovernor}, fee_calculator::{FeeCalculator, FeeRateGovernor},
@ -10,7 +10,6 @@ use solana_transaction_status::ConfirmedTransactionStatusWithSignature;
use std::{collections::HashMap, net::SocketAddr}; use std::{collections::HashMap, net::SocketAddr};
pub type RpcResult<T> = client_error::Result<Response<T>>; pub type RpcResult<T> = client_error::Result<Response<T>>;
pub type RpcAmount = String;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcResponseContext { pub struct RpcResponseContext {
@ -222,20 +221,12 @@ pub struct RpcStakeActivation {
pub inactive: u64, 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)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RpcTokenAccountBalance { pub struct RpcTokenAccountBalance {
pub address: String, pub address: String,
#[serde(flatten)] #[serde(flatten)]
pub amount: RpcTokenAmount, pub amount: UiTokenAmount,
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@ -9,7 +9,11 @@ use bincode::{config::Options, serialize};
use jsonrpc_core::{Error, Metadata, Result}; use jsonrpc_core::{Error, Metadata, Result};
use jsonrpc_derive::rpc; use jsonrpc_derive::rpc;
use solana_account_decoder::{ 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, UiAccount, UiAccountEncoding,
}; };
use solana_client::{ use solana_client::{
@ -242,8 +246,14 @@ impl JsonRpcRequestProcessor {
let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary);
new_response( new_response(
&bank, &bank,
bank.get_account(pubkey) bank.get_account(pubkey).and_then(|account| {
.map(|account| UiAccount::encode(account, encoding)), 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 config = config.unwrap_or_default();
let bank = self.bank(config.commitment); let bank = self.bank(config.commitment);
let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary);
get_filtered_program_accounts(&bank, program_id, filters) let keyed_accounts = get_filtered_program_accounts(&bank, program_id, filters);
.map(|(pubkey, account)| RpcKeyedAccount { if program_id == &spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed {
pubkey: pubkey.to_string(), get_parsed_token_accounts(bank, keyed_accounts).collect()
account: UiAccount::encode(account, encoding.clone()), } else {
}) keyed_accounts
.collect() .map(|(pubkey, account)| RpcKeyedAccount {
pubkey: pubkey.to_string(),
account: UiAccount::encode(account, encoding.clone(), None),
})
.collect()
}
} }
pub fn get_inflation_governor( pub fn get_inflation_governor(
@ -971,7 +986,7 @@ impl JsonRpcRequestProcessor {
&self, &self,
pubkey: &Pubkey, pubkey: &Pubkey,
commitment: Option<CommitmentConfig>, commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> { ) -> Result<RpcResponse<UiTokenAmount>> {
let bank = self.bank(commitment); let bank = self.bank(commitment);
let account = bank.get_account(pubkey).ok_or_else(|| { let account = bank.get_account(pubkey).ok_or_else(|| {
Error::invalid_params("Invalid param: could not find account".to_string()) Error::invalid_params("Invalid param: could not find account".to_string())
@ -998,7 +1013,7 @@ impl JsonRpcRequestProcessor {
&self, &self,
mint: &Pubkey, mint: &Pubkey,
commitment: Option<CommitmentConfig>, commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> { ) -> Result<RpcResponse<UiTokenAmount>> {
let bank = self.bank(commitment); let bank = self.bank(commitment);
let (mint_owner, decimals) = get_mint_owner_and_decimals(&bank, mint)?; let (mint_owner, decimals) = get_mint_owner_and_decimals(&bank, mint)?;
if mint_owner != spl_token_id_v1_0() { if mint_owner != spl_token_id_v1_0() {
@ -1106,12 +1121,17 @@ impl JsonRpcRequestProcessor {
encoding: None, encoding: None,
})); }));
} }
let accounts = get_filtered_program_accounts(&bank, &token_program_id, filters) let keyed_accounts = get_filtered_program_accounts(&bank, &token_program_id, filters);
.map(|(pubkey, account)| RpcKeyedAccount { let accounts = if encoding == UiAccountEncoding::JsonParsed {
pubkey: pubkey.to_string(), get_parsed_token_accounts(bank.clone(), keyed_accounts).collect()
account: UiAccount::encode(account, encoding.clone()), } else {
}) keyed_accounts
.collect(); .map(|(pubkey, account)| RpcKeyedAccount {
pubkey: pubkey.to_string(),
account: UiAccount::encode(account, encoding.clone(), None),
})
.collect()
};
Ok(new_response(&bank, accounts)) Ok(new_response(&bank, accounts))
} }
@ -1152,12 +1172,17 @@ impl JsonRpcRequestProcessor {
encoding: None, encoding: None,
})); }));
} }
let accounts = get_filtered_program_accounts(&bank, &token_program_id, filters) let keyed_accounts = get_filtered_program_accounts(&bank, &token_program_id, filters);
.map(|(pubkey, account)| RpcKeyedAccount { let accounts = if encoding == UiAccountEncoding::JsonParsed {
pubkey: pubkey.to_string(), get_parsed_token_accounts(bank.clone(), keyed_accounts).collect()
account: UiAccount::encode(account, encoding.clone()), } else {
}) keyed_accounts
.collect(); .map(|(pubkey, account)| RpcKeyedAccount {
pubkey: pubkey.to_string(),
account: UiAccount::encode(account, encoding.clone(), None),
})
.collect()
};
Ok(new_response(&bank, accounts)) Ok(new_response(&bank, accounts))
} }
} }
@ -1211,6 +1236,47 @@ fn get_filtered_program_accounts(
}) })
} }
pub(crate) fn get_parsed_token_account(bank: Arc<Bank>, account: Account) -> Option<UiAccount> {
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<I>(
bank: Arc<Bank>,
keyed_accounts: I,
) -> impl Iterator<Item = RpcKeyedAccount>
where
I: Iterator<Item = (Pubkey, Account)>,
{
let mut mint_decimals: HashMap<Pubkey, u8> = 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 /// Analyze a passed Pubkey that may be a Token program id or Mint address to determine the program
/// id and optional Mint /// id and optional Mint
fn get_token_program_id_and_mint( fn get_token_program_id_and_mint(
@ -1264,16 +1330,6 @@ fn get_mint_decimals(data: &[u8]) -> Result<u8> {
.map(|mint: &mut Mint| mint.decimals) .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] #[rpc]
pub trait RpcSol { pub trait RpcSol {
type Metadata; type Metadata;
@ -1565,7 +1621,7 @@ pub trait RpcSol {
meta: Self::Metadata, meta: Self::Metadata,
pubkey_str: String, pubkey_str: String,
commitment: Option<CommitmentConfig>, commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>>; ) -> Result<RpcResponse<UiTokenAmount>>;
#[rpc(meta, name = "getTokenSupply")] #[rpc(meta, name = "getTokenSupply")]
fn get_token_supply( fn get_token_supply(
@ -1573,7 +1629,7 @@ pub trait RpcSol {
meta: Self::Metadata, meta: Self::Metadata,
mint_str: String, mint_str: String,
commitment: Option<CommitmentConfig>, commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>>; ) -> Result<RpcResponse<UiTokenAmount>>;
#[rpc(meta, name = "getTokenLargestAccounts")] #[rpc(meta, name = "getTokenLargestAccounts")]
fn get_token_largest_accounts( fn get_token_largest_accounts(
@ -2226,7 +2282,7 @@ impl RpcSol for RpcSolImpl {
meta: Self::Metadata, meta: Self::Metadata,
pubkey_str: String, pubkey_str: String,
commitment: Option<CommitmentConfig>, commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> { ) -> Result<RpcResponse<UiTokenAmount>> {
debug!( debug!(
"get_token_account_balance rpc request received: {:?}", "get_token_account_balance rpc request received: {:?}",
pubkey_str pubkey_str
@ -2240,7 +2296,7 @@ impl RpcSol for RpcSolImpl {
meta: Self::Metadata, meta: Self::Metadata,
mint_str: String, mint_str: String,
commitment: Option<CommitmentConfig>, commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> { ) -> Result<RpcResponse<UiTokenAmount>> {
debug!("get_token_supply rpc request received: {:?}", mint_str); debug!("get_token_supply rpc request received: {:?}", mint_str);
let mint = verify_pubkey(mint_str)?; let mint = verify_pubkey(mint_str)?;
meta.get_token_supply(&mint, commitment) meta.get_token_supply(&mint, commitment)
@ -4614,7 +4670,7 @@ pub mod tests {
let res = io.handle_request_sync(&req, meta.clone()); let res = io.handle_request_sync(&req, meta.clone());
let result: Value = serde_json::from_str(&res.expect("actual response")) let result: Value = serde_json::from_str(&res.expect("actual response"))
.expect("actual response deserialization"); .expect("actual response deserialization");
let balance: RpcTokenAmount = let balance: UiTokenAmount =
serde_json::from_value(result["result"]["value"].clone()).unwrap(); serde_json::from_value(result["result"]["value"].clone()).unwrap();
let error = f64::EPSILON; let error = f64::EPSILON;
assert!((balance.ui_amount - 4.2).abs() < error); 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 res = io.handle_request_sync(&req, meta.clone());
let result: Value = serde_json::from_str(&res.expect("actual response")) let result: Value = serde_json::from_str(&res.expect("actual response"))
.expect("actual response deserialization"); .expect("actual response deserialization");
let supply: RpcTokenAmount = let supply: UiTokenAmount =
serde_json::from_value(result["result"]["value"].clone()).unwrap(); serde_json::from_value(result["result"]["value"].clone()).unwrap();
let error = f64::EPSILON; let error = f64::EPSILON;
assert!((supply.ui_amount - 2.0 * 4.2).abs() < error); assert!((supply.ui_amount - 2.0 * 4.2).abs() < error);
@ -4902,7 +4958,7 @@ pub mod tests {
vec![ vec![
RpcTokenAccountBalance { RpcTokenAccountBalance {
address: token_with_different_mint_pubkey.to_string(), address: token_with_different_mint_pubkey.to_string(),
amount: RpcTokenAmount { amount: UiTokenAmount {
ui_amount: 0.42, ui_amount: 0.42,
decimals: 2, decimals: 2,
amount: "42".to_string(), amount: "42".to_string(),
@ -4910,7 +4966,7 @@ pub mod tests {
}, },
RpcTokenAccountBalance { RpcTokenAccountBalance {
address: token_with_smaller_balance.to_string(), address: token_with_smaller_balance.to_string(),
amount: RpcTokenAmount { amount: UiTokenAmount {
ui_amount: 0.1, ui_amount: 0.1,
decimals: 2, decimals: 2,
amount: "10".to_string(), amount: "10".to_string(),

View File

@ -676,7 +676,8 @@ mod tests {
.get_account(&nonce_account.pubkey()) .get_account(&nonce_account.pubkey())
.unwrap() .unwrap()
.data; .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!({ let expected = json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": "accountNotification", "method": "accountNotification",

View File

@ -1,5 +1,6 @@
//! The `pubsub` module implements a threaded subscription service on client RPC request //! 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 core::hash::Hash;
use jsonrpc_core::futures::Future; use jsonrpc_core::futures::Future;
use jsonrpc_pubsub::{ use jsonrpc_pubsub::{
@ -7,7 +8,7 @@ use jsonrpc_pubsub::{
SubscriptionId, SubscriptionId,
}; };
use serde::Serialize; 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::{ use solana_client::{
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}, rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
rpc_filter::RpcFilterType, rpc_filter::RpcFilterType,
@ -178,7 +179,7 @@ where
K: Eq + Hash + Clone + Copy, K: Eq + Hash + Clone + Copy,
S: Clone + Serialize, S: Clone + Serialize,
B: Fn(&Bank, &K) -> X, B: Fn(&Bank, &K) -> X,
F: Fn(X, Slot, Option<T>) -> (Box<dyn Iterator<Item = S>>, Slot), F: Fn(X, Slot, Option<T>, Option<Arc<Bank>>) -> (Box<dyn Iterator<Item = S>>, Slot),
X: Clone + Serialize + Default, X: Clone + Serialize + Default,
T: Clone, T: Clone,
{ {
@ -202,16 +203,18 @@ where
commitment_slots.highest_confirmed_slot commitment_slots.highest_confirmed_slot
} }
}; };
let results = { let bank = bank_forks.read().unwrap().get(slot).cloned();
let bank_forks = bank_forks.read().unwrap(); let results = bank
bank_forks .clone()
.get(slot) .map(|desired_bank| bank_method(&desired_bank, hashmap_key))
.map(|desired_bank| bank_method(&desired_bank, hashmap_key)) .unwrap_or_default();
.unwrap_or_default()
};
let mut w_last_notified_slot = last_notified_slot.write().unwrap(); let mut w_last_notified_slot = last_notified_slot.write().unwrap();
let (filter_results, result_slot) = let (filter_results, result_slot) = filter_results(
filter_results(results, *w_last_notified_slot, config.as_ref().cloned()); results,
*w_last_notified_slot,
config.as_ref().cloned(),
bank,
);
for result in filter_results { for result in filter_results {
notifier.notify( notifier.notify(
Response { Response {
@ -244,16 +247,24 @@ fn filter_account_result(
result: Option<(Account, Slot)>, result: Option<(Account, Slot)>,
last_notified_slot: Slot, last_notified_slot: Slot,
encoding: Option<UiAccountEncoding>, encoding: Option<UiAccountEncoding>,
bank: Option<Arc<Bank>>,
) -> (Box<dyn Iterator<Item = UiAccount>>, Slot) { ) -> (Box<dyn Iterator<Item = UiAccount>>, Slot) {
if let Some((account, fork)) = result { if let Some((account, fork)) = result {
// If fork < last_notified_slot this means that we last notified for a fork // If fork < last_notified_slot this means that we last notified for a fork
// and should notify that the account state has been reverted. // and should notify that the account state has been reverted.
if fork != last_notified_slot { if fork != last_notified_slot {
let encoding = encoding.unwrap_or(UiAccountEncoding::Binary); let encoding = encoding.unwrap_or(UiAccountEncoding::Binary);
return ( if account.owner == spl_token_id_v1_0() && encoding == UiAccountEncoding::JsonParsed {
Box::new(iter::once(UiAccount::encode(account, encoding))), let bank = bank.unwrap(); // If result.is_some(), bank must also be Some
fork, 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) (Box::new(iter::empty()), last_notified_slot)
@ -263,6 +274,7 @@ fn filter_signature_result(
result: Option<transaction::Result<()>>, result: Option<transaction::Result<()>>,
last_notified_slot: Slot, last_notified_slot: Slot,
_config: Option<()>, _config: Option<()>,
_bank: Option<Arc<Bank>>,
) -> (Box<dyn Iterator<Item = RpcSignatureResult>>, Slot) { ) -> (Box<dyn Iterator<Item = RpcSignatureResult>>, Slot) {
( (
Box::new( Box::new(
@ -278,27 +290,30 @@ fn filter_program_results(
accounts: Vec<(Pubkey, Account)>, accounts: Vec<(Pubkey, Account)>,
last_notified_slot: Slot, last_notified_slot: Slot,
config: Option<ProgramConfig>, config: Option<ProgramConfig>,
bank: Option<Arc<Bank>>,
) -> (Box<dyn Iterator<Item = RpcKeyedAccount>>, Slot) { ) -> (Box<dyn Iterator<Item = RpcKeyedAccount>>, Slot) {
let config = config.unwrap_or_default(); let config = config.unwrap_or_default();
let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary); let encoding = config.encoding.unwrap_or(UiAccountEncoding::Binary);
let filters = config.filters; let filters = config.filters;
( let keyed_accounts = accounts.into_iter().filter(move |(_, account)| {
Box::new( filters.iter().all(|filter_type| match filter_type {
accounts RpcFilterType::DataSize(size) => account.data.len() as u64 == *size,
.into_iter() RpcFilterType::Memcmp(compare) => compare.bytes_match(&account.data),
.filter(move |(_, account)| { })
filters.iter().all(|filter_type| match filter_type { });
RpcFilterType::DataSize(size) => account.data.len() as u64 == *size, let accounts: Box<dyn Iterator<Item = RpcKeyedAccount>> =
RpcFilterType::Memcmp(compare) => compare.bytes_match(&account.data), 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))
.map(move |(pubkey, account)| RpcKeyedAccount { } else {
Box::new(
keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount {
pubkey: pubkey.to_string(), 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)] #[derive(Clone)]
@ -860,7 +875,7 @@ impl RpcSubscriptions {
&subscriptions.gossip_account_subscriptions, &subscriptions.gossip_account_subscriptions,
&subscriptions.gossip_program_subscriptions, &subscriptions.gossip_program_subscriptions,
&subscriptions.gossip_signature_subscriptions, &subscriptions.gossip_signature_subscriptions,
&bank_forks, bank_forks,
&commitment_slots, &commitment_slots,
&notifier, &notifier,
); );
@ -881,7 +896,7 @@ impl RpcSubscriptions {
for pubkey in &pubkeys { for pubkey in &pubkeys {
Self::check_account( Self::check_account(
pubkey, pubkey,
&bank_forks, bank_forks,
account_subscriptions.clone(), account_subscriptions.clone(),
&notifier, &notifier,
&commitment_slots, &commitment_slots,
@ -895,7 +910,7 @@ impl RpcSubscriptions {
for program_id in &programs { for program_id in &programs {
Self::check_program( Self::check_program(
program_id, program_id,
&bank_forks, bank_forks,
program_subscriptions.clone(), program_subscriptions.clone(),
&notifier, &notifier,
&commitment_slots, &commitment_slots,
@ -909,7 +924,7 @@ impl RpcSubscriptions {
for signature in &signatures { for signature in &signatures {
Self::check_signature( Self::check_signature(
signature, signature,
&bank_forks, bank_forks,
signature_subscriptions.clone(), signature_subscriptions.clone(),
&notifier, &notifier,
&commitment_slots, &commitment_slots,

View File

@ -1118,7 +1118,7 @@ The result will be an RpcResponse JSON object with `value` equal to an array of
// Request // 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 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 // 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 ### getTokenAccountsByOwner
@ -1154,7 +1154,7 @@ The result will be an RpcResponse JSON object with `value` equal to an array of
// Request // 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 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 // 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 ### getTokenSupply
@ -1481,8 +1481,10 @@ Subscribe to an account to receive notifications when the lamports or data for a
}, },
"value": { "value": {
"data": { "data": {
"nonce": { "program": "nonce"
"initialized": { "parsed": {
"type": "initialized",
"info": {
"authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX",
"blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k",
"feeCalculator": { "feeCalculator": {
@ -1597,8 +1599,10 @@ Subscribe to a program to receive notifications when the lamports or data for a
"pubkey": "H4vnBqifaSACnKa7acsxstsY1iV1bvJNxsCY7enrd1hq" "pubkey": "H4vnBqifaSACnKa7acsxstsY1iV1bvJNxsCY7enrd1hq"
"account": { "account": {
"data": { "data": {
"nonce": { "program": "nonce"
"initialized": { "parsed": {
"type": "initialized",
"info": {
"authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX",
"blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k",
"feeCalculator": { "feeCalculator": {