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
11 changed files with 326 additions and 144 deletions

View File

@ -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<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> {
) -> Result<RpcResponse<UiTokenAmount>> {
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<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> {
) -> Result<RpcResponse<UiTokenAmount>> {
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<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
/// id and optional 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)
}
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<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>>;
) -> Result<RpcResponse<UiTokenAmount>>;
#[rpc(meta, name = "getTokenSupply")]
fn get_token_supply(
@ -1573,7 +1629,7 @@ pub trait RpcSol {
meta: Self::Metadata,
mint_str: String,
commitment: Option<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>>;
) -> Result<RpcResponse<UiTokenAmount>>;
#[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<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> {
) -> Result<RpcResponse<UiTokenAmount>> {
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<CommitmentConfig>,
) -> Result<RpcResponse<RpcTokenAmount>> {
) -> Result<RpcResponse<UiTokenAmount>> {
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(),

View File

@ -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",

View File

@ -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<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,
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<UiAccountEncoding>,
bank: Option<Arc<Bank>>,
) -> (Box<dyn Iterator<Item = UiAccount>>, 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<transaction::Result<()>>,
last_notified_slot: Slot,
_config: Option<()>,
_bank: Option<Arc<Bank>>,
) -> (Box<dyn Iterator<Item = RpcSignatureResult>>, Slot) {
(
Box::new(
@ -278,27 +290,30 @@ fn filter_program_results(
accounts: Vec<(Pubkey, Account)>,
last_notified_slot: Slot,
config: Option<ProgramConfig>,
bank: Option<Arc<Bank>>,
) -> (Box<dyn Iterator<Item = RpcKeyedAccount>>, 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<dyn Iterator<Item = RpcKeyedAccount>> =
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,
&notifier,
);
@ -881,7 +896,7 @@ impl RpcSubscriptions {
for pubkey in &pubkeys {
Self::check_account(
pubkey,
&bank_forks,
bank_forks,
account_subscriptions.clone(),
&notifier,
&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(),
&notifier,
&commitment_slots,
@ -909,7 +924,7 @@ impl RpcSubscriptions {
for signature in &signatures {
Self::check_signature(
signature,
&bank_forks,
bank_forks,
signature_subscriptions.clone(),
&notifier,
&commitment_slots,