diff --git a/token/cli/src/main.rs b/token/cli/src/main.rs index db4f4b00..f96edf48 100644 --- a/token/cli/src/main.rs +++ b/token/cli/src/main.rs @@ -1626,6 +1626,7 @@ async fn command_address( async fn command_display(config: &Config<'_>, address: Pubkey) -> CommandResult { let account_data = config.get_account_checked(&address).await?; + let epoch_info = config.rpc_client.get_epoch_info().await?; let decimals = if let Some(mint_address) = get_token_account_mint(&account_data.data) { Some(config.get_mint_info(&mint_address, None).await?.decimals) @@ -1648,6 +1649,7 @@ async fn command_display(config: &Config<'_>, address: Pubkey) -> CommandResult let cli_output = CliTokenAccount { address: address.to_string(), program_id: config.program_id.to_string(), + epoch: epoch_info.epoch, decimals, is_associated: associated_address == address, account, @@ -1658,6 +1660,7 @@ async fn command_display(config: &Config<'_>, address: Pubkey) -> CommandResult Ok(TokenAccountType::Mint(mint)) => { let cli_output = CliMint { address: address.to_string(), + epoch: epoch_info.epoch, program_id: config.program_id.to_string(), mint, }; diff --git a/token/cli/src/output.rs b/token/cli/src/output.rs index fc7d3533..e4019a05 100644 --- a/token/cli/src/output.rs +++ b/token/cli/src/output.rs @@ -1,11 +1,11 @@ use crate::{config::Config, sort::UnsupportedAccount}; -use console::Emoji; +use console::{style, Emoji}; use serde::{Deserialize, Serialize, Serializer}; use solana_account_decoder::{ parse_token::{UiAccountState, UiMint, UiMultisig, UiTokenAccount, UiTokenAmount}, parse_token_extension::{ - UiDefaultAccountState, UiExtension, UiMemoTransfer, UiMintCloseAuthority, - UiTransferFeeAmount, + UiDefaultAccountState, UiExtension, UiInterestBearingConfig, UiMemoTransfer, + UiMintCloseAuthority, UiTransferFee, UiTransferFeeAmount, UiTransferFeeConfig, }, }; use solana_cli_output::{display::writeln_name_value, OutputFormat, QuietDisplay, VerboseDisplay}; @@ -168,11 +168,11 @@ impl fmt::Display for CliMultisig { let n = self.multisig.num_valid_signers; writeln!(f)?; - writeln_name_value(f, "SPL Token Multisig", " ")?; + writeln!(f, "{}", style("SPL Token Multisig").bold())?; writeln_name_value(f, " Address:", &self.address)?; writeln_name_value(f, " Program:", &self.program_id)?; writeln_name_value(f, " M/N:", &format!("{}/{}", m, n))?; - writeln_name_value(f, " Signers:", " ")?; + writeln!(f, " {}", style("Signers:").bold())?; let width = if n >= 9 { 4 } else { 3 }; for i in 0..n as usize { let title = format!(" {1:>0$}:", width, i + 1); @@ -188,6 +188,7 @@ impl fmt::Display for CliMultisig { pub(crate) struct CliTokenAccount { pub(crate) address: String, pub(crate) program_id: String, + pub(crate) epoch: u64, pub(crate) decimals: Option, pub(crate) is_associated: bool, #[serde(flatten)] @@ -200,7 +201,7 @@ impl VerboseDisplay for CliTokenAccount {} impl fmt::Display for CliTokenAccount { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f)?; - writeln_name_value(f, "SPL Token Account", " ")?; + writeln!(f, "{}", style("SPL Token Account").bold())?; if self.is_associated { writeln_name_value(f, " Address:", &self.address)?; } else { @@ -235,7 +236,7 @@ impl fmt::Display for CliTokenAccount { writeln_name_value(f, " Owner:", &self.account.owner)?; writeln_name_value(f, " State:", &format!("{:?}", self.account.state))?; if let Some(delegate) = &self.account.delegate { - writeln_name_value(f, " Delegation:", " ")?; + writeln!(f, " {}", style("Delegation:").bold())?; writeln_name_value(f, " Delegate:", delegate)?; let allowance = self.account.delegated_amount.as_ref().unwrap(); writeln_name_value(f, " Allowance:", &allowance.real_number_string_trimmed())?; @@ -252,9 +253,9 @@ impl fmt::Display for CliTokenAccount { )?; if !self.account.extensions.is_empty() { - writeln_name_value(f, "Extensions", " ")?; + writeln!(f, "{}", style("Extensions:").bold())?; for extension in &self.account.extensions { - display_ui_extension(f, extension)?; + display_ui_extension(f, self.epoch, extension)?; } } @@ -272,6 +273,7 @@ impl fmt::Display for CliTokenAccount { pub(crate) struct CliMint { pub(crate) address: String, pub(crate) program_id: String, + pub(crate) epoch: u64, #[serde(flatten)] pub(crate) mint: UiMint, } @@ -282,7 +284,7 @@ impl VerboseDisplay for CliMint {} impl fmt::Display for CliMint { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f)?; - writeln_name_value(f, "SPL Token Mint", " ")?; + writeln!(f, "{}", style("SPL Token Mint").bold())?; writeln_name_value(f, " Address:", &self.address)?; writeln_name_value(f, " Program:", &self.program_id)?; @@ -303,9 +305,9 @@ impl fmt::Display for CliMint { )?; if !self.mint.extensions.is_empty() { - writeln_name_value(f, "Extensions", " ")?; + writeln!(f, "{}", style("Extensions").bold())?; for extension in &self.mint.extensions { - display_ui_extension(f, extension)?; + display_ui_extension(f, self.epoch, extension)?; } } @@ -479,9 +481,54 @@ impl fmt::Display for CliTokenAccounts { } } -fn display_ui_extension(f: &mut fmt::Formatter, ui_extension: &UiExtension) -> fmt::Result { +fn display_ui_extension( + f: &mut fmt::Formatter, + epoch: u64, + ui_extension: &UiExtension, +) -> fmt::Result { match ui_extension { - UiExtension::TransferFeeConfig(_) => unimplemented!(), // annoying + UiExtension::TransferFeeConfig(UiTransferFeeConfig { + transfer_fee_config_authority, + withdraw_withheld_authority, + withheld_amount, + older_transfer_fee, + newer_transfer_fee, + .. + }) => { + let UiTransferFee { + transfer_fee_basis_points, + maximum_fee, + .. + } = if newer_transfer_fee.epoch >= epoch { + newer_transfer_fee + } else { + older_transfer_fee + }; + + writeln!(f, " {}", style("Transfer fees:").bold())?; + writeln!( + f, + " {} {}bps", + style("Fee:").bold(), + transfer_fee_basis_points + )?; + writeln_name_value(f, " Maximum fee:", &maximum_fee.to_string())?; + writeln_name_value( + f, + " Config authority:", + transfer_fee_config_authority + .as_ref() + .unwrap_or(&String::new()), + )?; + writeln_name_value( + f, + " Withdrawal authority:", + withdraw_withheld_authority + .as_ref() + .unwrap_or(&String::new()), + )?; + writeln_name_value(f, " Withheld fees:", &withheld_amount.to_string()) + } UiExtension::TransferFeeAmount(UiTransferFeeAmount { withheld_amount }) => { writeln_name_value(f, " Transfer fees withheld:", &withheld_amount.to_string()) } @@ -497,7 +544,7 @@ fn display_ui_extension(f: &mut fmt::Formatter, ui_extension: &UiExtension) -> f UiExtension::DefaultAccountState(UiDefaultAccountState { account_state }) => { writeln_name_value(f, " Default state:", &format!("{:?}", account_state)) } - UiExtension::ImmutableOwner => writeln_name_value(f, " Immutable owner", " "), + UiExtension::ImmutableOwner => writeln!(f, " {}", style("Immutable owner").bold()), UiExtension::MemoTransfer(UiMemoTransfer { require_incoming_transfer_memos, }) => writeln_name_value( @@ -509,8 +556,25 @@ fn display_ui_extension(f: &mut fmt::Formatter, ui_extension: &UiExtension) -> f "Not required" }, ), - UiExtension::NonTransferable => writeln_name_value(f, " Non-transferable", " "), - UiExtension::InterestBearingConfig(_) => unimplemented!(), // little annoying + UiExtension::NonTransferable => writeln!(f, " {}", style("Non-transferable").bold()), + UiExtension::InterestBearingConfig(UiInterestBearingConfig { + rate_authority, + current_rate, + .. + }) => { + writeln!(f, " {}", style("Interest-bearing:").bold())?; + writeln!( + f, + " {} {}bps", + style("Interest rate:").bold(), + current_rate + )?; + writeln_name_value( + f, + " Rate authority:", + rate_authority.as_ref().unwrap_or(&String::new()), + ) + } UiExtension::UnparseableExtension => panic!("err here"), UiExtension::Uninitialized => panic!("err here...?"), } diff --git a/token/cli/src/sort.rs b/token/cli/src/sort.rs index bf4d6c7b..adaf110a 100644 --- a/token/cli/src/sort.rs +++ b/token/cli/src/sort.rs @@ -56,6 +56,7 @@ pub(crate) fn sort_and_parse_token_accounts( let parsed_account = CliTokenAccount { address, program_id: program_id.to_string(), + epoch: 0, decimals: None, account: ui_token_account, is_associated,