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

@ -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<AccountAdditionalData>,
) -> 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()

View File

@ -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<u8>,
}
pub fn parse_account_data(
program_id: &Pubkey,
data: &[u8],
additional_data: Option<AccountAdditionalData>,
) -> Result<ParsedAccount, ParseAccountError> {
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<u8> = 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());
}
}

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 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<TokenAccountType, ParseAccountError> {
pub fn parse_token(
data: &[u8],
mint_decimals: Option<u8>,
) -> Result<TokenAccountType, ParseAccountError> {
let mut data = data.to_vec();
if data.len() == size_of::<Account>() {
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<String>,
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<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)]
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::<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)
);
}
}