diff --git a/Cargo.lock b/Cargo.lock index 8713135508..a92e7b9bf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3589,6 +3589,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "solana-account-decoder", "solana-clap-utils", "solana-client", "solana-sdk 1.3.14", diff --git a/account-decoder/src/parse_token.rs b/account-decoder/src/parse_token.rs index ee526349e7..b28954fb46 100644 --- a/account-decoder/src/parse_token.rs +++ b/account-decoder/src/parse_token.rs @@ -154,6 +154,31 @@ pub struct UiTokenAmount { pub amount: StringAmount, } +impl UiTokenAmount { + pub fn real_number_string(&self) -> String { + let decimals = self.decimals as usize; + if decimals > 0 { + let amount = u64::from_str(&self.amount).unwrap_or(0); + + // Left-pad zeros to decimals + 1, so we at least have an integer zero + let mut s = format!("{:01$}", amount, decimals + 1); + + // Add the decimal point (Sorry, "," locales!) + s.insert(s.len() - decimals, '.'); + s + } else { + self.amount.clone() + } + } + + pub fn real_number_string_trimmed(&self) -> String { + let s = self.real_number_string(); + let zeros_trimmed = s.trim_end_matches('0'); + let decimal_trimmed = zeros_trimmed.trim_end_matches('.'); + decimal_trimmed.to_string() + } +} + 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; @@ -296,4 +321,20 @@ mod test { Some(expected_mint_pubkey) ); } + + #[test] + fn test_ui_token_amount_real_string() { + let token_amount = token_amount_to_ui_amount(1, 0); + assert_eq!(&token_amount.real_number_string(), "1"); + assert_eq!(&token_amount.real_number_string_trimmed(), "1"); + let token_amount = token_amount_to_ui_amount(1, 9); + assert_eq!(&token_amount.real_number_string(), "0.000000001"); + assert_eq!(&token_amount.real_number_string_trimmed(), "0.000000001"); + let token_amount = token_amount_to_ui_amount(1_000_000_000, 9); + assert_eq!(&token_amount.real_number_string(), "1.000000000"); + assert_eq!(&token_amount.real_number_string_trimmed(), "1"); + let token_amount = token_amount_to_ui_amount(1_234_567_890, 3); + assert_eq!(&token_amount.real_number_string(), "1234567.890"); + assert_eq!(&token_amount.real_number_string_trimmed(), "1234567.89"); + } } diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index c0f81ba0ba..768092109c 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -17,6 +17,7 @@ indicatif = "0.15.0" serde = "1.0.112" serde_derive = "1.0.103" serde_json = "1.0.56" +solana-account-decoder = { path = "../account-decoder", version = "1.3.14" } solana-clap-utils = { path = "../clap-utils", version = "1.3.14" } solana-client = { path = "../client", version = "1.3.14" } solana-sdk = { path = "../sdk", version = "1.3.14" } diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index f5f3f20c3a..1e594587c2 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -4,6 +4,7 @@ use console::{style, Emoji}; use inflector::cases::titlecase::to_title_case; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; +use solana_account_decoder::parse_token::UiTokenAccount; use solana_clap_utils::keypair::SignOnly; use solana_client::rpc_response::{ RpcAccountBalance, RpcKeyedAccount, RpcSupply, RpcVoteAccountInfo, @@ -1149,6 +1150,47 @@ impl fmt::Display for CliFees { } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliTokenAccount { + pub address: String, + #[serde(flatten)] + pub token_account: UiTokenAccount, +} + +impl fmt::Display for CliTokenAccount { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + writeln_name_value(f, "Address:", &self.address)?; + let account = &self.token_account; + writeln_name_value( + f, + "Balance:", + &account.token_amount.real_number_string_trimmed(), + )?; + let mint = format!( + "{}{}", + account.mint, + if account.is_native { " (native)" } else { "" } + ); + writeln_name_value(f, "Mint:", &mint)?; + writeln_name_value(f, "Owner:", &account.owner)?; + writeln_name_value(f, "State:", &format!("{:?}", account.state))?; + if let Some(delegate) = &account.delegate { + writeln!(f, "Delegation:")?; + writeln_name_value(f, " Delegate:", delegate)?; + let allowance = account.delegated_amount.as_ref().unwrap(); + writeln_name_value(f, " Allowance:", &allowance.real_number_string_trimmed())?; + } + writeln_name_value( + f, + "Close authority:", + &account.close_authority.as_ref().unwrap_or(&String::new()), + )?; + Ok(()) + } +} + pub fn return_signers( tx: &Transaction, output_format: &OutputFormat,