From 85bcec3c1184d4030ae78ca30100e793c0c4a9b6 Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 1 Apr 2026 13:30:43 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Add=20printers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fuzz/src/invariant.rs | 21 + crates/fuzz/src/trident/flow_executor.rs | 2 + crates/fuzz/src/trident/mod.rs | 3 +- crates/fuzz/src/trident/print.rs | 595 ++++++++++++++++++ crates/fuzz/src/trident/progress.rs | 28 + documentation/docs/index.md | 2 + .../docs/trident-advanced/printers/index.md | 48 ++ documentation/docs/trident-api/index.md | 4 + documentation/docs/trident-api/printers.md | 253 ++++++++ documentation/mkdocs.yml | 4 + examples/token/trident-tests/Cargo.lock | 3 +- .../token/trident-tests/fuzz_0/test_fuzz.rs | 4 + 12 files changed, 965 insertions(+), 2 deletions(-) create mode 100644 crates/fuzz/src/trident/print.rs create mode 100644 documentation/docs/trident-advanced/printers/index.md create mode 100644 documentation/docs/trident-api/printers.md diff --git a/crates/fuzz/src/invariant.rs b/crates/fuzz/src/invariant.rs index 1b108a066..c82aa66a3 100644 --- a/crates/fuzz/src/invariant.rs +++ b/crates/fuzz/src/invariant.rs @@ -262,3 +262,24 @@ macro_rules! invariant_lte { } }; } + +/// Progress-bar-safe print macro. Use this instead of `println!` or `eprintln!` +/// inside fuzz flows to avoid garbled output in parallel mode. +/// +/// In parallel mode, the output is routed through the progress bar channel +/// so it never mixes with the progress bar or with prints from other threads. +/// In single-thread mode, it falls back to `eprintln!`. +/// +/// # Examples +/// +/// ```ignore +/// tprintln!("balance: {}", balance); +/// tprintln!("account: {:#?}", account_data); +/// tprintln!("before: {}, after: {}", before.amount, after.amount); +/// ``` +#[macro_export] +macro_rules! tlog { + ($($arg:tt)*) => { + $crate::trident::progress::send_user_log(format!($($arg)*)) + }; +} diff --git a/crates/fuzz/src/trident/flow_executor.rs b/crates/fuzz/src/trident/flow_executor.rs index 28d77e241..0458ba252 100644 --- a/crates/fuzz/src/trident/flow_executor.rs +++ b/crates/fuzz/src/trident/flow_executor.rs @@ -640,6 +640,8 @@ fn run_thread_workload_impl( .trident_mut() .set_master_seed_and_thread_id(master_seed, thread_id); + progress::set_user_log_sender(event_tx.clone()); + // Track progress updates to avoid excessive bar updates let mut last_update = Instant::now(); let mut local_counter = 0u64; diff --git a/crates/fuzz/src/trident/mod.rs b/crates/fuzz/src/trident/mod.rs index e59ca3a65..84ffc29bf 100644 --- a/crates/fuzz/src/trident/mod.rs +++ b/crates/fuzz/src/trident/mod.rs @@ -14,7 +14,8 @@ pub mod flow_executor; mod system; mod metrics; -mod progress; +mod print; +pub mod progress; mod random; mod seed; #[cfg(feature = "stake")] diff --git a/crates/fuzz/src/trident/print.rs b/crates/fuzz/src/trident/print.rs new file mode 100644 index 000000000..3918addd4 --- /dev/null +++ b/crates/fuzz/src/trident/print.rs @@ -0,0 +1,595 @@ +use std::fmt; + +use borsh::BorshDeserialize; +use solana_sdk::pubkey::Pubkey; +use trident_svm::prelude::TridentTransactionResult; + +use crate::trident::progress; +use crate::AccountDiscriminator; + +use super::Trident; + +fn colors_enabled() -> bool { + use std::io::IsTerminal; + std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none() +} + +fn cyan(text: &str) -> String { + if colors_enabled() { + format!("\x1b[36m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn dim(text: &str) -> String { + if colors_enabled() { + format!("\x1b[2m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn green(text: &str) -> String { + if colors_enabled() { + format!("\x1b[32m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn red(text: &str) -> String { + if colors_enabled() { + format!("\x1b[31m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn yellow(text: &str) -> String { + if colors_enabled() { + format!("\x1b[33m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn short_pubkey(key: &Pubkey) -> String { + let s = key.to_string(); + if s.len() > 11 { + format!("{}..{}", &s[..4], &s[s.len() - 4..]) + } else { + s + } +} + +fn header(title: &str, key: &Pubkey) -> String { + let label = format!("{} ({})", title, short_pubkey(key)); + let bar_len = 50usize.saturating_sub(label.len() + 3); + let bar = "─".repeat(bar_len); + dim(&format!("── {} {}", label, bar)) +} + +fn header_no_key(title: &str) -> String { + let bar_len = 50usize.saturating_sub(title.len() + 3); + let bar = "─".repeat(bar_len); + dim(&format!("── {} {}", title, bar)) +} + +impl Trident { + /// Prints a formatted transaction result including status, compute units, and logs. + /// + /// Output is routed through the progress bar channel in parallel mode + /// so it never mixes with the progress bar. + /// + /// # Example + /// ```rust,ignore + /// let result = self.trident.process_transaction(&[ix], Some("transfer")); + /// self.trident.print_transaction_result(&result); + /// ``` + pub fn print_transaction_result(&self, result: &TridentTransactionResult) { + let mut out = String::new(); + out.push_str(&header_no_key("Transaction Result")); + out.push('\n'); + + let status_str = if result.is_success() { + green("OK") + } else { + let err = match result.status() { + Ok(()) => "unknown".to_string(), + Err(e) => e.to_string(), + }; + red(&format!("FAILED: {}", err)) + }; + out.push_str(&format!(" Status: {}\n", status_str)); + out.push_str(&format!(" CU used: {}\n", result.compute_units_consumed())); + out.push_str(&format!( + " Timestamp: {}\n", + result.transaction_timestamp() + )); + + let logs = result.logs(); + if !logs.is_empty() { + out.push_str(&format!(" {}\n", dim("Logs:"))); + for line in logs.lines() { + out.push_str(&format!(" {}\n", dim(line))); + } + } + + progress::send_user_log(out); + } + + /// Prints a deserialized program account with colored formatting. + /// + /// Fetches the account, deserializes it as type `T`, and pretty-prints + /// the `Debug` output. If the account doesn't exist or deserialization + /// fails, prints an error message instead. + /// + /// # Example + /// ```rust,ignore + /// self.trident.print_account::(&account_key, Some(8)); + /// ``` + pub fn print_account( + &mut self, + key: &Pubkey, + discriminator_size_override: Option, + ) { + let mut out = String::new(); + out.push_str(&header("Account", key)); + out.push('\n'); + + match self.get_account_with_type::(key, discriminator_size_override) { + Some(account) => { + let formatted = format!("{:#?}", account); + for line in formatted.lines() { + out.push_str(&format!(" {}\n", cyan(line))); + } + } + None => { + out.push_str(&format!( + " {}\n", + yellow("Account not found or deserialization failed") + )); + } + } + + progress::send_user_log(out); + } + + /// Prints raw account metadata without deserialization. + /// + /// Shows owner, lamports (with SOL conversion), data length, and + /// executable flag. Works for any account — wallets, PDAs, unknown + /// layouts, or accounts with no data at all. + /// + /// # Example + /// ```rust,ignore + /// self.trident.print_raw_account(&some_pubkey); + /// ``` + pub fn print_raw_account(&self, key: &Pubkey) { + use solana_sdk::account::ReadableAccount; + + let account = self.get_account(key); + let mut out = String::new(); + out.push_str(&header("Account", key)); + out.push('\n'); + + if account.data().is_empty() && account.lamports() == 0 { + out.push_str(&format!(" {}\n", yellow("Account not found"))); + progress::send_user_log(out); + return; + } + + out.push_str(&format!(" Owner: {}\n", cyan(&account.owner().to_string()))); + + let lamports = account.lamports(); + let sol = lamports as f64 / 1_000_000_000.0; + if sol >= 0.001 { + out.push_str(&format!( + " Lamports: {} {}\n", + cyan(&lamports.to_string()), + dim(&format!("({:.4} SOL)", sol)), + )); + } else { + out.push_str(&format!(" Lamports: {}\n", cyan(&lamports.to_string()))); + } + + out.push_str(&format!(" Data: {} bytes\n", cyan(&account.data().len().to_string()))); + out.push_str(&format!(" Executable: {}\n", cyan(&account.executable().to_string()))); + + progress::send_user_log(out); + } + + /// Prints program account details with colored formatting. + /// + /// Automatically detects the loader type (v3 upgradeable, v4, v2, native) + /// and prints the relevant metadata. For v3 programs, fetches and prints + /// both the program account and its ProgramData account (authority, slot). + /// Binary data is never printed. + /// + /// # Example + /// ```rust,ignore + /// self.trident.print_program(&program_id); + /// ``` + pub fn print_program(&mut self, key: &Pubkey) { + use solana_sdk::account::ReadableAccount; + + let account = self.get_account(key); + let mut out = String::new(); + out.push_str(&header("Program", key)); + out.push('\n'); + + if account.data().is_empty() && account.lamports() == 0 { + out.push_str(&format!(" {}\n", yellow("Account not found"))); + progress::send_user_log(out); + return; + } + + let owner = account.owner(); + out.push_str(&format!(" Owner: {}\n", cyan(&owner.to_string()))); + out.push_str(&format!(" Executable: {}\n", cyan(&account.executable().to_string()))); + out.push_str(&format!(" Lamports: {}\n", cyan(&account.lamports().to_string()))); + out.push_str(&format!(" Data: {} bytes\n", cyan(&account.data().len().to_string()))); + + if *owner == solana_sdk::bpf_loader_upgradeable::ID { + self.print_program_v3(&account, &mut out); + } else if *owner == solana_sdk::loader_v4::ID { + Self::print_program_v4(&account, &mut out); + } else if *owner == solana_sdk::bpf_loader::ID { + out.push_str(&format!(" Loader: {}\n", cyan("BPF Loader v2 (non-upgradeable)"))); + out.push_str(&format!(" ELF: {} bytes {}\n", + cyan(&account.data().len().to_string()), + dim("(not shown)"), + )); + } else if *owner == solana_sdk::native_loader::ID { + out.push_str(&format!(" Loader: {}\n", cyan("Native Loader (built-in)"))); + } else { + out.push_str(&format!(" {}\n", + yellow(&format!("Unknown program owner: {}", owner)), + )); + } + + progress::send_user_log(out); + } + + fn print_program_v3( + &mut self, + account: &solana_sdk::account::AccountSharedData, + out: &mut String, + ) { + use solana_loader_v3_interface::state::UpgradeableLoaderState; + use solana_sdk::account::ReadableAccount; + + out.push_str(&format!(" Loader: {}\n", cyan("BPF Loader v3 (upgradeable)"))); + + let state: Result = bincode::deserialize(account.data()); + match state { + Ok(UpgradeableLoaderState::Program { programdata_address }) => { + out.push_str(&format!( + " ProgramData: {}\n", + cyan(&programdata_address.to_string()), + )); + + let pd_account = self.get_account(&programdata_address); + if pd_account.data().is_empty() { + out.push_str(&format!( + " {}\n", + yellow("ProgramData account not found"), + )); + return; + } + + out.push('\n'); + out.push_str(&format!(" {}\n", dim(&format!( + "── ProgramData ({}) ──", + short_pubkey(&programdata_address), + )))); + + let pd_state: Result = + bincode::deserialize(pd_account.data()); + match pd_state { + Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address, + }) => { + out.push_str(&format!(" Slot: {}\n", cyan(&slot.to_string()))); + let auth_str = match upgrade_authority_address { + Some(a) => a.to_string(), + None => "None (immutable)".to_string(), + }; + out.push_str(&format!(" Authority: {}\n", cyan(&auth_str))); + const PROGRAM_DATA_METADATA_SIZE: usize = 45; + let elf_len = pd_account.data().len().saturating_sub(PROGRAM_DATA_METADATA_SIZE); + out.push_str(&format!( + " ELF: {} bytes {}\n", + cyan(&elf_len.to_string()), + dim("(not shown)"), + )); + out.push_str(&format!(" Lamports: {}\n", cyan(&pd_account.lamports().to_string()))); + } + _ => { + out.push_str(&format!( + " {}\n", + yellow("Failed to deserialize ProgramData state"), + )); + } + } + } + Ok(UpgradeableLoaderState::Buffer { authority_address }) => { + out.push_str(&format!(" Type: {}\n", cyan("Buffer"))); + let auth_str = match authority_address { + Some(a) => a.to_string(), + None => "None".to_string(), + }; + out.push_str(&format!(" Authority: {}\n", cyan(&auth_str))); + } + Ok(UpgradeableLoaderState::Uninitialized) => { + out.push_str(&format!(" Type: {}\n", cyan("Uninitialized"))); + } + _ => { + out.push_str(&format!( + " {}\n", + yellow("Failed to deserialize v3 loader state"), + )); + } + } + } + + fn print_program_v4( + account: &solana_sdk::account::AccountSharedData, + out: &mut String, + ) { + use solana_sdk::account::ReadableAccount; + use solana_sdk::loader_v4::{LoaderV4State, LoaderV4Status}; + + out.push_str(&format!(" Loader: {}\n", cyan("Loader v4"))); + + let data = account.data(); + let header_size = LoaderV4State::program_data_offset(); + if data.len() < header_size { + out.push_str(&format!( + " {}\n", + yellow("Account data too small for LoaderV4State header"), + )); + return; + } + + // SAFETY: LoaderV4State is #[repr(C)], Copy, and we verified the buffer is large enough. + let state: &LoaderV4State = + unsafe { &*(data.as_ptr() as *const LoaderV4State) }; + + out.push_str(&format!(" Slot: {}\n", cyan(&state.slot.to_string()))); + + let status_str = match state.status { + LoaderV4Status::Retracted => "Retracted (maintenance)", + LoaderV4Status::Deployed => "Deployed", + LoaderV4Status::Finalized => "Finalized (immutable)", + }; + out.push_str(&format!(" Status: {}\n", cyan(status_str))); + + let auth_label = match state.status { + LoaderV4Status::Finalized => "Next version", + _ => "Authority", + }; + out.push_str(&format!( + " {}: {}\n", + auth_label, + cyan(&state.authority_address_or_next_version.to_string()), + )); + + let elf_len = data.len().saturating_sub(header_size); + out.push_str(&format!( + " ELF: {} bytes {}\n", + cyan(&elf_len.to_string()), + dim("(not shown)"), + )); + } +} + +#[cfg(feature = "token")] +use crate::trident::token2022::{MintExtensionData, TokenAccountExtensionData}; + +#[cfg(feature = "token")] +fn pod_pubkey(p: &spl_pod::optional_keys::OptionalNonZeroPubkey) -> String { + let key: Option = Option::from(*p); + match key { + Some(k) => k.to_string(), + None => "None".to_string(), + } +} + +#[cfg(feature = "token")] +fn format_token_extension(ext: &TokenAccountExtensionData) -> String { + match ext { + TokenAccountExtensionData::TransferFeeAmount(e) => { + format!("TransferFeeAmount: withheld = {}", u64::from(e.withheld_amount)) + } + TokenAccountExtensionData::ImmutableOwner(_) => "ImmutableOwner".to_string(), + TokenAccountExtensionData::NonTransferableAccount(_) => "NonTransferableAccount".to_string(), + TokenAccountExtensionData::TransferHookAccount(_) => "TransferHookAccount".to_string(), + TokenAccountExtensionData::PausableAccount(_) => "PausableAccount".to_string(), + TokenAccountExtensionData::MemoTransfer(e) => { + format!("MemoTransfer: require_incoming = {}", bool::from(e.require_incoming_transfer_memos)) + } + TokenAccountExtensionData::CpiGuard(e) => { + format!("CpiGuard: lock = {}", bool::from(e.lock_cpi)) + } + TokenAccountExtensionData::Unknown(t) => format!("Unknown({:?})", t), + } +} + +#[cfg(feature = "token")] +fn format_mint_extension(ext: &MintExtensionData) -> String { + match ext { + MintExtensionData::TransferFeeConfig(e) => { + let bp = u16::from(e.newer_transfer_fee.transfer_fee_basis_points); + let max = u64::from(e.newer_transfer_fee.maximum_fee); + let bp_pct = bp as f64 / 100.0; + format!("TransferFeeConfig: {}% ({} bps), max_fee = {}", bp_pct, bp, max) + } + MintExtensionData::MintCloseAuthority(e) => { + format!("MintCloseAuthority: {}", pod_pubkey(&e.close_authority)) + } + MintExtensionData::DefaultAccountState(e) => { + let state = u8::from(e.state); + let label = match state { + 0 => "Uninitialized", + 1 => "Initialized", + 2 => "Frozen", + _ => "Unknown", + }; + format!("DefaultAccountState: {} ({})", label, state) + } + MintExtensionData::NonTransferable(_) => "NonTransferable".to_string(), + MintExtensionData::InterestBearingConfig(e) => { + format!("InterestBearingConfig: rate = {} bps", i16::from(e.current_rate)) + } + MintExtensionData::PermanentDelegate(e) => { + format!("PermanentDelegate: {}", pod_pubkey(&e.delegate)) + } + MintExtensionData::TransferHook(e) => { + format!("TransferHook: program = {}", pod_pubkey(&e.program_id)) + } + MintExtensionData::MetadataPointer(e) => { + format!("MetadataPointer: {}", pod_pubkey(&e.metadata_address)) + } + MintExtensionData::GroupPointer(e) => { + format!("GroupPointer: {}", pod_pubkey(&e.group_address)) + } + MintExtensionData::GroupMemberPointer(e) => { + format!("GroupMemberPointer: {}", pod_pubkey(&e.member_address)) + } + MintExtensionData::ScaledUiAmount(e) => { + format!("ScaledUiAmount: multiplier = {}", f64::from(e.multiplier)) + } + MintExtensionData::Pausable(_) => "Pausable".to_string(), + MintExtensionData::TokenMetadata(e) => { + let mut s = format!("TokenMetadata: name = \"{}\", symbol = \"{}\"", e.name, e.symbol); + if !e.uri.is_empty() { + s.push_str(&format!(", uri = \"{}\"", e.uri)); + } + let auth: Option = Option::from(e.update_authority); + if let Some(a) = auth { + s.push_str(&format!(", update_authority = {}", a)); + } + s + } + MintExtensionData::TokenGroup(e) => { + format!("TokenGroup: size = {}/{}", u64::from(e.size), u64::from(e.max_size)) + } + MintExtensionData::TokenGroupMember(e) => { + format!( + "TokenGroupMember: group = {}, member_number = {}", + e.group, u64::from(e.member_number) + ) + } + MintExtensionData::Unknown(t) => format!("Unknown({:?})", t), + } +} + +#[cfg(feature = "token")] +impl Trident { + /// Prints a token account with colored formatting showing mint, owner, amount, + /// delegate, state, and any Token-2022 extensions. + /// + /// Fetches the token account (works with both SPL Token and Token-2022) + /// and prints the key fields. If the account doesn't exist or isn't a + /// valid token account, prints an error message. + /// + /// # Example + /// ```rust,ignore + /// self.trident.print_token_account(token_account_pubkey); + /// ``` + pub fn print_token_account(&mut self, account: Pubkey) { + let mut out = String::new(); + out.push_str(&header("Token Account", &account)); + out.push('\n'); + + match self.get_token_account(account) { + Ok(token_acc) => { + let acc = &token_acc.account; + out.push_str(&format!(" Mint: {}\n", cyan(&acc.mint.to_string()))); + out.push_str(&format!(" Owner: {}\n", cyan(&acc.owner.to_string()))); + out.push_str(&format!(" Amount: {}\n", green(&acc.amount.to_string()))); + + let delegate_str = match acc.delegate { + solana_sdk::program_option::COption::Some(d) => { + format!("{} (delegated: {})", d, acc.delegated_amount) + } + solana_sdk::program_option::COption::None => "None".to_string(), + }; + out.push_str(&format!(" Delegate: {}\n", dim(&delegate_str))); + + let state_str = format!("{:?}", acc.state); + out.push_str(&format!(" State: {}\n", dim(&state_str))); + + if !token_acc.extensions.is_empty() { + out.push_str(&format!(" {} ({})\n", cyan("Extensions"), token_acc.extensions.len())); + for ext in &token_acc.extensions { + out.push_str(&format!(" {}\n", dim(&format_token_extension(ext)))); + } + } + } + Err(e) => { + out.push_str(&format!( + " {}\n", + yellow(&format!("Failed to read token account: {:?}", e)) + )); + } + } + + progress::send_user_log(out); + } + + /// Prints a mint account with colored formatting showing supply, decimals, + /// authorities, and any Token-2022 extensions. + /// + /// Fetches the mint (works with both SPL Token and Token-2022) + /// and prints the key fields. If the account doesn't exist or isn't a + /// valid mint, prints an error message. + /// + /// # Example + /// ```rust,ignore + /// self.trident.print_mint_account(mint_pubkey); + /// ``` + pub fn print_mint_account(&mut self, account: Pubkey) { + let mut out = String::new(); + out.push_str(&header("Mint", &account)); + out.push('\n'); + + match self.get_mint(account) { + Ok(mint_data) => { + let m = &mint_data.mint; + + let authority_str = match m.mint_authority { + solana_sdk::program_option::COption::Some(a) => a.to_string(), + solana_sdk::program_option::COption::None => "None (fixed supply)".to_string(), + }; + out.push_str(&format!(" Authority: {}\n", cyan(&authority_str))); + out.push_str(&format!(" Supply: {}\n", green(&m.supply.to_string()))); + out.push_str(&format!(" Decimals: {}\n", cyan(&m.decimals.to_string()))); + + let freeze_str = match m.freeze_authority { + solana_sdk::program_option::COption::Some(a) => a.to_string(), + solana_sdk::program_option::COption::None => "None".to_string(), + }; + out.push_str(&format!(" Freeze: {}\n", dim(&freeze_str))); + + if !mint_data.extensions.is_empty() { + out.push_str(&format!(" {} ({})\n", cyan("Extensions"), mint_data.extensions.len())); + for ext in &mint_data.extensions { + out.push_str(&format!(" {}\n", dim(&format_mint_extension(ext)))); + } + } + } + Err(e) => { + out.push_str(&format!( + " {}\n", + yellow(&format!("Failed to read mint account: {:?}", e)) + )); + } + } + + progress::send_user_log(out); + } +} diff --git a/crates/fuzz/src/trident/progress.rs b/crates/fuzz/src/trident/progress.rs index 917de7aa4..678919866 100644 --- a/crates/fuzz/src/trident/progress.rs +++ b/crates/fuzz/src/trident/progress.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::io::IsTerminal; use std::sync::mpsc; use std::thread; @@ -9,6 +10,30 @@ pub(crate) enum WorkerEvent { ProgressDelta(u64), InvariantFailure(String), ProgramPanicsDelta(u64), + UserLog(String), +} + +thread_local! { + static USER_LOG_TX: RefCell>> = const { RefCell::new(None) }; +} + +pub(crate) fn set_user_log_sender(tx: mpsc::Sender) { + USER_LOG_TX.with(|cell| { + *cell.borrow_mut() = Some(tx); + }); +} + +/// Prints a message safely, routing through the progress bar channel in parallel mode +/// to avoid garbling the progress bar output. +pub fn send_user_log(msg: String) { + USER_LOG_TX.with(|cell| { + let guard = cell.borrow(); + if let Some(ref tx) = *guard { + let _ = tx.send(WorkerEvent::UserLog(msg)); + } else { + eprintln!("{}", msg); + } + }); } /// Final aggregated runtime summary produced by the UI/controller thread. @@ -264,6 +289,9 @@ pub(crate) fn spawn_parallel_ui_controller( WorkerEvent::ProgramPanicsDelta(delta) => { program_panics += delta; } + WorkerEvent::UserLog(msg) => { + main_pb.println(&msg); + } } main_pb.set_message(format_live_status_parallel( diff --git a/documentation/docs/index.md b/documentation/docs/index.md index b8f5f236c..be77c84ca 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -17,6 +17,8 @@ Explore the various features Trident provides. - [Invariants and Assertions](./trident-advanced/invariants-assertions/index.md) - Validate program behavior with custom invariants and assertions - [Multi-Instruction Transactions](./trident-advanced/multi-instruction-transactions/index.md) - Execute multiple instructions within a single transaction + - [Forking](./trident-advanced/forking/index.md) - Fork on-chain state for fuzzing against real program data + - [Printers](./trident-advanced/printers/index.md) - Debug with formatted output for accounts, transactions, and programs - [Trident Manifest](./trident-manifest/index.md) - Customize your fuzz tests with different configurations - [Code Coverage](./trident-advanced/code-coverage/index.md) - Track and display fuzz test code coverage - [Dashboard](./trident-advanced/dashboard/index.md) - A web-based interface for visualizing fuzzing session results diff --git a/documentation/docs/trident-advanced/printers/index.md b/documentation/docs/trident-advanced/printers/index.md new file mode 100644 index 000000000..a91d6bbca --- /dev/null +++ b/documentation/docs/trident-advanced/printers/index.md @@ -0,0 +1,48 @@ +# Printers + +Trident provides built-in printer methods for debugging during fuzzing. All output is routed through a safe channel that prevents mixing with the progress bar in parallel mode. + +## `tlog!` Macro + +Use `tlog!` for ad-hoc debug printing. It works like `println!` but is safe to use during fuzzing — output won't collide with the progress bar. + +```rust +tlog!("balance: {}", balance); +tlog!("account: {:#?}", account_data); +``` + +!!! note + Prefer `tlog!` over `println!` or `eprintln!` in your fuzz tests. Direct prints will mix with the progress bar in parallel mode. + +In addition to `tlog!`, Trident provides several print methods on the `Trident` struct for inspecting transaction results, data accounts, token accounts, mint accounts, and programs. See the [Printers API Reference](../../trident-api/printers.md) for the full list with signatures and output examples. + +## Example + +```rust +#[flow] +fn debug_flow(&mut self) { + let ix = create_my_instruction(&self.trident); + let result = self.trident.process_transaction(&[ix], Some("my_ix")); + + // Print the transaction result + self.trident.print_transaction_result(&result); + + // Print a deserialized data account + self.trident.print_account::(&account_key, None); + + // Print raw account metadata (no deserialization needed) + self.trident.print_raw_account(&wallet_key); + + // Print program info (auto-detects loader v2/v3/v4) + self.trident.print_program(&program_id); + + // Print token accounts (requires `token` feature) + self.trident.print_token_account(token_account_key); + self.trident.print_mint_account(mint_key); + + // Ad-hoc printing + tlog!("custom value: {}", my_value); +} +``` + +For detailed method signatures and output examples, see the [Printers API Reference](../../trident-api/printers.md). diff --git a/documentation/docs/trident-api/index.md b/documentation/docs/trident-api/index.md index abf6c4c19..6e079ba82 100644 --- a/documentation/docs/trident-api/index.md +++ b/documentation/docs/trident-api/index.md @@ -93,6 +93,10 @@ pub fn get_last_blockhash(&self) -> Hash - **[System Program Methods](./system-program.md)** - Account creation, allocation, assignment, and SOL transfers +### Debugging + +- **[Printers](./printers.md)** - Print formatted account data, transaction results, and program details + ### Feature-Gated Methods - **[SPL Token Methods](./spl-token.md)** - Available with `token` feature diff --git a/documentation/docs/trident-api/printers.md b/documentation/docs/trident-api/printers.md new file mode 100644 index 000000000..15d65f132 --- /dev/null +++ b/documentation/docs/trident-api/printers.md @@ -0,0 +1,253 @@ +# Printers + +Methods for printing formatted account data, transaction results, and program details during fuzzing. All output is routed through a safe channel to avoid mixing with the progress bar. + +## `tlog!` + +Ad-hoc debug printing macro. Works like `println!` but safe to use during parallel fuzzing. + +```rust +tlog!("balance: {}", balance); +tlog!("account: {:#?}", account_data); +tlog!("before: {}, after: {}", before.amount, after.amount); +``` + +!!! note + Prefer `tlog!` over `println!` or `eprintln!` in your fuzz tests. Direct prints will mix with the progress bar in parallel mode. + +--- + +## `print_transaction_result` + +Prints a formatted transaction result including status, compute units, timestamp, and logs. + +```rust +pub fn print_transaction_result(&self, result: &TridentTransactionResult) +``` + +**Parameters:** + +- `result` - The transaction result returned by `process_transaction` + +**Example:** + +```rust +let result = self.trident.process_transaction(&[ix], Some("transfer")); +self.trident.print_transaction_result(&result); +``` + +**Output:** + +``` +── Transaction Result ───────────────────────────── + Status: OK + CU used: 12345 + Timestamp: 1234567890 + Logs: + Program 11111... invoke [1] + Program 11111... success +``` + +--- + +## `print_account` + +Fetches, deserializes, and pretty-prints a data account. The type must implement `BorshDeserialize`, `AccountDiscriminator`, and `Debug`. + +```rust +pub fn print_account( + &mut self, + key: &Pubkey, + discriminator_size_override: Option, +) +``` + +**Parameters:** + +- `key` - The public key of the account +- `discriminator_size_override` - Optional override for discriminator size to skip. If `None`, uses the length from the type's `AccountDiscriminator` implementation. + +**Example:** + +```rust +// Use discriminator from AccountDiscriminator trait +self.trident.print_account::(&account_key, None); + +// Override discriminator size (e.g. for non-Anchor accounts) +self.trident.print_account::(&account_key, Some(0)); +``` + +**Output:** + +``` +── Account (7xKX..m3Qp) ────────────────────────── + MyAccountType { + balance: 1000000, + owner: 7xKX...m3Qp, + is_initialized: true, + } +``` + +--- + +## `print_raw_account` + +Prints raw account metadata without deserialization. Works for any account — wallets, PDAs, unknown layouts, or accounts with no data. + +```rust +pub fn print_raw_account(&self, key: &Pubkey) +``` + +**Parameters:** + +- `key` - The public key of the account + +**Example:** + +```rust +self.trident.print_raw_account(&some_pubkey); +``` + +**Output:** + +``` +── Account (7xKX..m3Qp) ────────────────────────── + Owner: 11111111111111111111111111111111 + Lamports: 1000000000 (1.0000 SOL) + Data: 0 bytes + Executable: false +``` + +--- + +## `print_program` + +Prints program account details. Automatically detects the loader type and prints relevant metadata. For upgradeable programs (v3), both the program account and its ProgramData account are printed. The ELF binary is never shown. + +```rust +pub fn print_program(&mut self, key: &Pubkey) +``` + +**Parameters:** + +- `key` - The public key of the program + +**Supported loaders:** + +| Loader | Details shown | +|---|---| +| BPF Loader v3 (upgradeable) | Program + ProgramData (authority, slot, ELF size) | +| Loader v4 | Status (Deployed/Retracted/Finalized), authority, slot, ELF size | +| BPF Loader v2 (non-upgradeable) | Basic metadata | +| Native Loader | Identifies built-in programs | + +**Example:** + +```rust +self.trident.print_program(&program_id); +``` + +**Output (v3 upgradeable):** + +``` +── Program (6Kd1..Aq3V) ────────────────────────── + Owner: BPFLoaderUpgradeab1e111111111111111111111111 + Executable: true + Lamports: 1141440 + Data: 36 bytes + Loader: BPF Loader v3 (upgradeable) + ProgramData: 3Hq8kR...Xm2P + + ── ProgramData (3Hq8..Xm2P) ── + Slot: 285102847 + Authority: jvzRJsqnPtEfiSkotmGsoFt7kKzVqtyJaqYiWduZoht + ELF: 245760 bytes (not shown) + Lamports: 1830480 +``` + +**Output (v4):** + +``` +── Program (9xQe..Bm7P) ────────────────────────── + Owner: LoaderV411111111111111111111111111111111111 + Executable: true + Lamports: 2500000 + Data: 131120 bytes + Loader: Loader v4 + Slot: 300000000 + Status: Deployed + Authority: jvzRJsqnPtEfiSkotmGsoFt7kKzVqtyJaqYiWduZoht + ELF: 131072 bytes (not shown) +``` + +--- + +## `print_token_account` + +!!! note "Requires `token` feature" + +Prints a token account showing mint, owner, amount, delegate, state, and any Token-2022 extensions. + +```rust +pub fn print_token_account(&mut self, account: Pubkey) +``` + +**Parameters:** + +- `account` - The public key of the token account + +**Example:** + +```rust +self.trident.print_token_account(token_account_pubkey); +``` + +**Output:** + +``` +── Token Account (9xQe..Bm7P) ──────────────────── + Mint: EPjFW...4Cph + Owner: 7xKX...m3Qp + Amount: 1000000 + Delegate: None + State: Initialized + Extensions (2) + ImmutableOwner + TransferFeeAmount: withheld = 0 +``` + +--- + +## `print_mint_account` + +!!! note "Requires `token` feature" + +Prints a mint account showing authority, supply, decimals, freeze authority, and any Token-2022 extensions with human-readable values. + +```rust +pub fn print_mint_account(&mut self, account: Pubkey) +``` + +**Parameters:** + +- `account` - The public key of the mint account + +**Example:** + +```rust +self.trident.print_mint_account(mint_pubkey); +``` + +**Output:** + +``` +── Mint (EPjF..4Cph) ───────────────────────────── + Authority: jvzRJsqnPtEfiSkotmGsoFt7kKzVqtyJaqYiWduZoht + Supply: 1000000000 + Decimals: 6 + Freeze: None + Extensions (3) + TransferFeeConfig: 1% (100 bps), max_fee = 1000000 + MetadataPointer: E9pCkonLma6xWWwXLvNUnS48W6ncFBX8VEnj4Eru3rjC + TokenMetadata: name = "My Token", symbol = "MTK", uri = "https://..." +``` \ No newline at end of file diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 8439eef06..b718bc8a7 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -29,6 +29,9 @@ nav: - trident-advanced/invariants-assertions/index.md - Multi-Instruction Transactions: - trident-advanced/multi-instruction-transactions/index.md + - Debugging: + - Printers: + - trident-advanced/printers/index.md - External State: - Forking: - trident-advanced/forking/index.md @@ -50,6 +53,7 @@ nav: - trident-api/token-2022.md - trident-api/vote-program.md - trident-api/stake-program.md + - trident-api/printers.md - Invariant Macros: - trident-api/invariants.md - Address Storage: diff --git a/examples/token/trident-tests/Cargo.lock b/examples/token/trident-tests/Cargo.lock index f42f0b0c4..68bb4151e 100644 --- a/examples/token/trident-tests/Cargo.lock +++ b/examples/token/trident-tests/Cargo.lock @@ -6873,7 +6873,8 @@ dependencies = [ [[package]] name = "trident-svm" version = "0.3.0-rc.3" -source = "git+https://github.com/Ackee-Blockchain/trident-svm?branch=develop#775588320f1c3d7b08f0127731db1884ee12cc7e" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe68c97abb435a48b39d766f9b6b6e776ee27d75fc5ff007d740793822f40cd9" dependencies = [ "agave-feature-set", "agave-precompiles", diff --git a/examples/token/trident-tests/fuzz_0/test_fuzz.rs b/examples/token/trident-tests/fuzz_0/test_fuzz.rs index ca20a1403..75e1bc2ce 100644 --- a/examples/token/trident-tests/fuzz_0/test_fuzz.rs +++ b/examples/token/trident-tests/fuzz_0/test_fuzz.rs @@ -787,6 +787,8 @@ impl FuzzTest { .trident .process_transaction(&ixs, Some("Multiple extensions")); + self.trident.print_mint_account(mint2022); + invariant!( res.is_success(), "GroupPointer + TokenGroup + MetadataPointer + TokenMetadata + Pausable + TransferFeeConfig + ScaledUiAmount + MintCloseAuthority extension failed: {:#?}", @@ -816,6 +818,8 @@ impl FuzzTest { ], ); + self.trident.print_token_account(token_account2022_1); + let res = self.trident.process_transaction( &ixs, Some("ImmutableOwner + CpiGuard + MemoTransfer extension"), From 0bb99d0e60133f0085d579a3c42aca6c48caa893 Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 1 Apr 2026 13:34:49 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20Changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a2120904..94a1fad9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ incremented upon a breaking change and the patch version will be incremented for **Added** +- add printers for printing account data, transaction results, and program details ([464](https://github.com/Ackee-Blockchain/trident/pull/464)) - add invariant macros for better invariant checking and error handling ([461](https://github.com/Ackee-Blockchain/trident/pull/461)) - introduce simple Fork Testing allowing forking and caching programs and accounts from desired clusters ([434](https://github.com/Ackee-Blockchain/trident/pull/434)) - add new random generation methods ([444](https://github.com/Ackee-Blockchain/trident/pull/444)) From 46b23ecc25916bd3d479c11186495acd1cfe0ac1 Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 1 Apr 2026 14:19:22 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Format=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fuzz/src/trident/print.rs | 161 ++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 43 deletions(-) diff --git a/crates/fuzz/src/trident/print.rs b/crates/fuzz/src/trident/print.rs index 3918addd4..dc5ebc65a 100644 --- a/crates/fuzz/src/trident/print.rs +++ b/crates/fuzz/src/trident/print.rs @@ -102,7 +102,10 @@ impl Trident { red(&format!("FAILED: {}", err)) }; out.push_str(&format!(" Status: {}\n", status_str)); - out.push_str(&format!(" CU used: {}\n", result.compute_units_consumed())); + out.push_str(&format!( + " CU used: {}\n", + result.compute_units_consumed() + )); out.push_str(&format!( " Timestamp: {}\n", result.transaction_timestamp() @@ -180,7 +183,10 @@ impl Trident { return; } - out.push_str(&format!(" Owner: {}\n", cyan(&account.owner().to_string()))); + out.push_str(&format!( + " Owner: {}\n", + cyan(&account.owner().to_string()) + )); let lamports = account.lamports(); let sol = lamports as f64 / 1_000_000_000.0; @@ -194,8 +200,14 @@ impl Trident { out.push_str(&format!(" Lamports: {}\n", cyan(&lamports.to_string()))); } - out.push_str(&format!(" Data: {} bytes\n", cyan(&account.data().len().to_string()))); - out.push_str(&format!(" Executable: {}\n", cyan(&account.executable().to_string()))); + out.push_str(&format!( + " Data: {} bytes\n", + cyan(&account.data().len().to_string()) + )); + out.push_str(&format!( + " Executable: {}\n", + cyan(&account.executable().to_string()) + )); progress::send_user_log(out); } @@ -227,24 +239,41 @@ impl Trident { let owner = account.owner(); out.push_str(&format!(" Owner: {}\n", cyan(&owner.to_string()))); - out.push_str(&format!(" Executable: {}\n", cyan(&account.executable().to_string()))); - out.push_str(&format!(" Lamports: {}\n", cyan(&account.lamports().to_string()))); - out.push_str(&format!(" Data: {} bytes\n", cyan(&account.data().len().to_string()))); + out.push_str(&format!( + " Executable: {}\n", + cyan(&account.executable().to_string()) + )); + out.push_str(&format!( + " Lamports: {}\n", + cyan(&account.lamports().to_string()) + )); + out.push_str(&format!( + " Data: {} bytes\n", + cyan(&account.data().len().to_string()) + )); if *owner == solana_sdk::bpf_loader_upgradeable::ID { self.print_program_v3(&account, &mut out); } else if *owner == solana_sdk::loader_v4::ID { Self::print_program_v4(&account, &mut out); } else if *owner == solana_sdk::bpf_loader::ID { - out.push_str(&format!(" Loader: {}\n", cyan("BPF Loader v2 (non-upgradeable)"))); - out.push_str(&format!(" ELF: {} bytes {}\n", + out.push_str(&format!( + " Loader: {}\n", + cyan("BPF Loader v2 (non-upgradeable)") + )); + out.push_str(&format!( + " ELF: {} bytes {}\n", cyan(&account.data().len().to_string()), dim("(not shown)"), )); } else if *owner == solana_sdk::native_loader::ID { - out.push_str(&format!(" Loader: {}\n", cyan("Native Loader (built-in)"))); + out.push_str(&format!( + " Loader: {}\n", + cyan("Native Loader (built-in)") + )); } else { - out.push_str(&format!(" {}\n", + out.push_str(&format!( + " {}\n", yellow(&format!("Unknown program owner: {}", owner)), )); } @@ -260,11 +289,16 @@ impl Trident { use solana_loader_v3_interface::state::UpgradeableLoaderState; use solana_sdk::account::ReadableAccount; - out.push_str(&format!(" Loader: {}\n", cyan("BPF Loader v3 (upgradeable)"))); + out.push_str(&format!( + " Loader: {}\n", + cyan("BPF Loader v3 (upgradeable)") + )); let state: Result = bincode::deserialize(account.data()); match state { - Ok(UpgradeableLoaderState::Program { programdata_address }) => { + Ok(UpgradeableLoaderState::Program { + programdata_address, + }) => { out.push_str(&format!( " ProgramData: {}\n", cyan(&programdata_address.to_string()), @@ -272,18 +306,18 @@ impl Trident { let pd_account = self.get_account(&programdata_address); if pd_account.data().is_empty() { - out.push_str(&format!( - " {}\n", - yellow("ProgramData account not found"), - )); + out.push_str(&format!(" {}\n", yellow("ProgramData account not found"),)); return; } out.push('\n'); - out.push_str(&format!(" {}\n", dim(&format!( - "── ProgramData ({}) ──", - short_pubkey(&programdata_address), - )))); + out.push_str(&format!( + " {}\n", + dim(&format!( + "── ProgramData ({}) ──", + short_pubkey(&programdata_address), + )) + )); let pd_state: Result = bincode::deserialize(pd_account.data()); @@ -299,13 +333,19 @@ impl Trident { }; out.push_str(&format!(" Authority: {}\n", cyan(&auth_str))); const PROGRAM_DATA_METADATA_SIZE: usize = 45; - let elf_len = pd_account.data().len().saturating_sub(PROGRAM_DATA_METADATA_SIZE); + let elf_len = pd_account + .data() + .len() + .saturating_sub(PROGRAM_DATA_METADATA_SIZE); out.push_str(&format!( " ELF: {} bytes {}\n", cyan(&elf_len.to_string()), dim("(not shown)"), )); - out.push_str(&format!(" Lamports: {}\n", cyan(&pd_account.lamports().to_string()))); + out.push_str(&format!( + " Lamports: {}\n", + cyan(&pd_account.lamports().to_string()) + )); } _ => { out.push_str(&format!( @@ -335,12 +375,10 @@ impl Trident { } } - fn print_program_v4( - account: &solana_sdk::account::AccountSharedData, - out: &mut String, - ) { + fn print_program_v4(account: &solana_sdk::account::AccountSharedData, out: &mut String) { use solana_sdk::account::ReadableAccount; - use solana_sdk::loader_v4::{LoaderV4State, LoaderV4Status}; + use solana_sdk::loader_v4::LoaderV4State; + use solana_sdk::loader_v4::LoaderV4Status; out.push_str(&format!(" Loader: {}\n", cyan("Loader v4"))); @@ -355,10 +393,12 @@ impl Trident { } // SAFETY: LoaderV4State is #[repr(C)], Copy, and we verified the buffer is large enough. - let state: &LoaderV4State = - unsafe { &*(data.as_ptr() as *const LoaderV4State) }; + let state: &LoaderV4State = unsafe { &*(data.as_ptr() as *const LoaderV4State) }; - out.push_str(&format!(" Slot: {}\n", cyan(&state.slot.to_string()))); + out.push_str(&format!( + " Slot: {}\n", + cyan(&state.slot.to_string()) + )); let status_str = match state.status { LoaderV4Status::Retracted => "Retracted (maintenance)", @@ -387,7 +427,9 @@ impl Trident { } #[cfg(feature = "token")] -use crate::trident::token2022::{MintExtensionData, TokenAccountExtensionData}; +use crate::trident::token2022::MintExtensionData; +#[cfg(feature = "token")] +use crate::trident::token2022::TokenAccountExtensionData; #[cfg(feature = "token")] fn pod_pubkey(p: &spl_pod::optional_keys::OptionalNonZeroPubkey) -> String { @@ -402,14 +444,22 @@ fn pod_pubkey(p: &spl_pod::optional_keys::OptionalNonZeroPubkey) -> String { fn format_token_extension(ext: &TokenAccountExtensionData) -> String { match ext { TokenAccountExtensionData::TransferFeeAmount(e) => { - format!("TransferFeeAmount: withheld = {}", u64::from(e.withheld_amount)) + format!( + "TransferFeeAmount: withheld = {}", + u64::from(e.withheld_amount) + ) } TokenAccountExtensionData::ImmutableOwner(_) => "ImmutableOwner".to_string(), - TokenAccountExtensionData::NonTransferableAccount(_) => "NonTransferableAccount".to_string(), + TokenAccountExtensionData::NonTransferableAccount(_) => { + "NonTransferableAccount".to_string() + } TokenAccountExtensionData::TransferHookAccount(_) => "TransferHookAccount".to_string(), TokenAccountExtensionData::PausableAccount(_) => "PausableAccount".to_string(), TokenAccountExtensionData::MemoTransfer(e) => { - format!("MemoTransfer: require_incoming = {}", bool::from(e.require_incoming_transfer_memos)) + format!( + "MemoTransfer: require_incoming = {}", + bool::from(e.require_incoming_transfer_memos) + ) } TokenAccountExtensionData::CpiGuard(e) => { format!("CpiGuard: lock = {}", bool::from(e.lock_cpi)) @@ -425,7 +475,10 @@ fn format_mint_extension(ext: &MintExtensionData) -> String { let bp = u16::from(e.newer_transfer_fee.transfer_fee_basis_points); let max = u64::from(e.newer_transfer_fee.maximum_fee); let bp_pct = bp as f64 / 100.0; - format!("TransferFeeConfig: {}% ({} bps), max_fee = {}", bp_pct, bp, max) + format!( + "TransferFeeConfig: {}% ({} bps), max_fee = {}", + bp_pct, bp, max + ) } MintExtensionData::MintCloseAuthority(e) => { format!("MintCloseAuthority: {}", pod_pubkey(&e.close_authority)) @@ -442,7 +495,10 @@ fn format_mint_extension(ext: &MintExtensionData) -> String { } MintExtensionData::NonTransferable(_) => "NonTransferable".to_string(), MintExtensionData::InterestBearingConfig(e) => { - format!("InterestBearingConfig: rate = {} bps", i16::from(e.current_rate)) + format!( + "InterestBearingConfig: rate = {} bps", + i16::from(e.current_rate) + ) } MintExtensionData::PermanentDelegate(e) => { format!("PermanentDelegate: {}", pod_pubkey(&e.delegate)) @@ -464,7 +520,10 @@ fn format_mint_extension(ext: &MintExtensionData) -> String { } MintExtensionData::Pausable(_) => "Pausable".to_string(), MintExtensionData::TokenMetadata(e) => { - let mut s = format!("TokenMetadata: name = \"{}\", symbol = \"{}\"", e.name, e.symbol); + let mut s = format!( + "TokenMetadata: name = \"{}\", symbol = \"{}\"", + e.name, e.symbol + ); if !e.uri.is_empty() { s.push_str(&format!(", uri = \"{}\"", e.uri)); } @@ -475,12 +534,17 @@ fn format_mint_extension(ext: &MintExtensionData) -> String { s } MintExtensionData::TokenGroup(e) => { - format!("TokenGroup: size = {}/{}", u64::from(e.size), u64::from(e.max_size)) + format!( + "TokenGroup: size = {}/{}", + u64::from(e.size), + u64::from(e.max_size) + ) } MintExtensionData::TokenGroupMember(e) => { format!( "TokenGroupMember: group = {}, member_number = {}", - e.group, u64::from(e.member_number) + e.group, + u64::from(e.member_number) ) } MintExtensionData::Unknown(t) => format!("Unknown({:?})", t), @@ -510,7 +574,10 @@ impl Trident { let acc = &token_acc.account; out.push_str(&format!(" Mint: {}\n", cyan(&acc.mint.to_string()))); out.push_str(&format!(" Owner: {}\n", cyan(&acc.owner.to_string()))); - out.push_str(&format!(" Amount: {}\n", green(&acc.amount.to_string()))); + out.push_str(&format!( + " Amount: {}\n", + green(&acc.amount.to_string()) + )); let delegate_str = match acc.delegate { solana_sdk::program_option::COption::Some(d) => { @@ -524,7 +591,11 @@ impl Trident { out.push_str(&format!(" State: {}\n", dim(&state_str))); if !token_acc.extensions.is_empty() { - out.push_str(&format!(" {} ({})\n", cyan("Extensions"), token_acc.extensions.len())); + out.push_str(&format!( + " {} ({})\n", + cyan("Extensions"), + token_acc.extensions.len() + )); for ext in &token_acc.extensions { out.push_str(&format!(" {}\n", dim(&format_token_extension(ext)))); } @@ -576,7 +647,11 @@ impl Trident { out.push_str(&format!(" Freeze: {}\n", dim(&freeze_str))); if !mint_data.extensions.is_empty() { - out.push_str(&format!(" {} ({})\n", cyan("Extensions"), mint_data.extensions.len())); + out.push_str(&format!( + " {} ({})\n", + cyan("Extensions"), + mint_data.extensions.len() + )); for ext in &mint_data.extensions { out.push_str(&format!(" {}\n", dim(&format_mint_extension(ext)))); }