From: Vihiga Tyonum Date: Fri, 24 Apr 2026 18:41:23 +0000 (+0100) Subject: refactor(handlers): Add types for outputting data X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/FromScriptException.UnrecognizedScript.html?a=commitdiff_plain;h=a9732732e4d55547489a9768134cdb4093b54fa7;p=bdk-cli refactor(handlers): Add types for outputting data - add trait for formatting outputs - add types for presenting data to the output - refactor offline mod to use types --- diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index f7a4737..7fb0179 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -4,6 +4,7 @@ pub mod key; pub mod offline; pub mod online; pub mod repl; +pub mod types; pub mod wallets; #[cfg(feature = "repl")] diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 9d564eb..6399413 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -1,16 +1,17 @@ use crate::commands::OfflineWalletSubCommand::*; use crate::commands::{CliOpts, OfflineWalletSubCommand, WalletOpts}; use crate::error::BDKCliError as Error; -use crate::utils::shorten; +use crate::handlers::types::{ + AddressResult, BalanceResult, PoliciesResult, PsbtResult, PublicDescriptorResult, RawPsbt, + TransactionDetails, TransactionListResult, UnspentDetails, UnspentListResult, +}; +use crate::utils::output::FormatOutput; use bdk_wallet::bitcoin::base64::Engine; use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD; use bdk_wallet::bitcoin::consensus::encode::serialize_hex; use bdk_wallet::bitcoin::script::PushBytesBuf; -use bdk_wallet::bitcoin::{Address, Amount, FeeRate, Psbt, Sequence, Txid}; -use bdk_wallet::chain::ChainPosition; +use bdk_wallet::bitcoin::{Amount, FeeRate, Psbt, Sequence, Txid}; use bdk_wallet::{KeychainKind, SignOptions, Wallet}; -use cli_table::format::Justify; -use cli_table::{Cell, CellStruct, Style, Table}; use serde_json::json; use std::collections::BTreeMap; use std::str::FromStr; @@ -24,232 +25,62 @@ pub fn handle_offline_wallet_subcommand( cli_opts: &CliOpts, offline_subcommand: OfflineWalletSubCommand, ) -> Result { + let pretty = cli_opts.pretty; match offline_subcommand { NewAddress => { let addr = wallet.reveal_next_address(KeychainKind::External); - if cli_opts.pretty { - let table = vec![ - vec!["Address".cell().bold(true), addr.address.to_string().cell()], - vec![ - "Index".cell().bold(true), - addr.index.to_string().cell().justify(Justify::Right), - ], - ] - .table() - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else if wallet_opts.verbose { - Ok(serde_json::to_string_pretty(&json!({ - "address": addr.address, - "index": addr.index - }))?) - } else { - Ok(serde_json::to_string_pretty(&json!({ - "address": addr.address, - }))?) - } + let result: AddressResult = addr.into(); + result.format(pretty) } UnusedAddress => { let addr = wallet.next_unused_address(KeychainKind::External); - - if cli_opts.pretty { - let table = vec![ - vec!["Address".cell().bold(true), addr.address.to_string().cell()], - vec![ - "Index".cell().bold(true), - addr.index.to_string().cell().justify(Justify::Right), - ], - ] - .table() - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else if wallet_opts.verbose { - Ok(serde_json::to_string_pretty(&json!({ - "address": addr.address, - "index": addr.index - }))?) - } else { - Ok(serde_json::to_string_pretty(&json!({ - "address": addr.address, - }))?) - } + let result: AddressResult = addr.into(); + result.format(pretty) } Unspent => { - let utxos = wallet.list_unspent().collect::>(); - if cli_opts.pretty { - let mut rows: Vec> = vec![]; - for utxo in &utxos { - let height = utxo - .chain_position - .confirmation_height_upper_bound() - .map(|h| h.to_string()) - .unwrap_or("Pending".to_string()); + let utxos: Vec = wallet + .list_unspent() + .map(|utxo| UnspentDetails::from_local_output(&utxo, cli_opts.network)) + .collect(); - let block_hash = match &utxo.chain_position { - ChainPosition::Confirmed { anchor, .. } => anchor.block_id.hash.to_string(), - ChainPosition::Unconfirmed { .. } => "Unconfirmed".to_string(), - }; - - rows.push(vec![ - shorten(utxo.outpoint, 8, 10).cell(), - utxo.txout - .value - .to_sat() - .to_string() - .cell() - .justify(Justify::Right), - Address::from_script(&utxo.txout.script_pubkey, cli_opts.network) - .unwrap() - .cell(), - utxo.keychain.cell(), - utxo.is_spent.cell(), - utxo.derivation_index.cell(), - height.to_string().cell().justify(Justify::Right), - shorten(block_hash, 8, 8).cell().justify(Justify::Right), - ]); - } - let table = rows - .table() - .title(vec![ - "Outpoint".cell().bold(true), - "Output (sat)".cell().bold(true), - "Output Address".cell().bold(true), - "Keychain".cell().bold(true), - "Is Spent".cell().bold(true), - "Index".cell().bold(true), - "Block Height".cell().bold(true), - "Block Hash".cell().bold(true), - ]) - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else { - Ok(serde_json::to_string_pretty(&utxos)?) - } + let result = UnspentListResult(utxos); + result.format(pretty) } Transactions => { let transactions = wallet.transactions(); - if cli_opts.pretty { - let txns = transactions - .map(|tx| { - let total_value = tx - .tx_node - .output - .iter() - .map(|output| output.value.to_sat()) - .sum::(); - ( - tx.tx_node.txid.to_string(), - tx.tx_node.version, - tx.tx_node.is_explicitly_rbf(), - tx.tx_node.input.len(), - tx.tx_node.output.len(), - total_value, - ) - }) - .collect::>(); - let mut rows: Vec> = vec![]; - for (txid, version, is_rbf, input_count, output_count, total_value) in txns { - rows.push(vec![ - txid.cell(), - version.to_string().cell().justify(Justify::Right), - is_rbf.to_string().cell().justify(Justify::Center), - input_count.to_string().cell().justify(Justify::Right), - output_count.to_string().cell().justify(Justify::Right), - total_value.to_string().cell().justify(Justify::Right), - ]); - } - let table = rows - .table() - .title(vec![ - "Txid".cell().bold(true), - "Version".cell().bold(true), - "Is RBF".cell().bold(true), - "Input Count".cell().bold(true), - "Output Count".cell().bold(true), - "Total Value (sat)".cell().bold(true), - ]) - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else { - let txns: Vec<_> = transactions - .map(|tx| { - json!({ - "txid": tx.tx_node.txid, - "is_coinbase": tx.tx_node.is_coinbase(), - "wtxid": tx.tx_node.compute_wtxid(), - "version": tx.tx_node.version, - "is_rbf": tx.tx_node.is_explicitly_rbf(), - "inputs": tx.tx_node.input, - "outputs": tx.tx_node.output, - }) - }) - .collect(); - Ok(serde_json::to_string_pretty(&txns)?) - } + let txns: Vec = transactions + .map(|tx| { + let total_value = tx + .tx_node + .output + .iter() + .map(|output| output.value.to_sat()) + .sum::(); + + TransactionDetails { + txid: tx.tx_node.txid.to_string(), + is_coinbase: tx.tx_node.is_coinbase(), + wtxid: tx.tx_node.compute_wtxid().to_string(), + version: serde_json::to_value(tx.tx_node.version).unwrap_or(json!(1)), + version_display: tx.tx_node.version.to_string(), + is_rbf: tx.tx_node.is_explicitly_rbf(), + inputs: serde_json::to_value(&tx.tx_node.input).unwrap_or_default(), + outputs: serde_json::to_value(&tx.tx_node.output).unwrap_or_default(), + input_count: tx.tx_node.input.len(), + output_count: tx.tx_node.output.len(), + total_value, + } + }) + .collect(); + + let result = TransactionListResult(txns); + result.format(pretty) } Balance => { let balance = wallet.balance(); - if cli_opts.pretty { - let table = vec![ - vec!["Type".cell().bold(true), "Amount (sat)".cell().bold(true)], - vec![ - "Total".cell(), - balance - .total() - .to_sat() - .to_string() - .cell() - .justify(Justify::Right), - ], - vec![ - "Confirmed".cell(), - balance - .confirmed - .to_sat() - .to_string() - .cell() - .justify(Justify::Right), - ], - vec![ - "Unconfirmed".cell(), - balance - .immature - .to_sat() - .to_string() - .cell() - .justify(Justify::Right), - ], - vec![ - "Trusted Pending".cell(), - balance - .trusted_pending - .to_sat() - .cell() - .justify(Justify::Right), - ], - vec![ - "Untrusted Pending".cell(), - balance - .untrusted_pending - .to_sat() - .cell() - .justify(Justify::Right), - ], - ] - .table() - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else { - Ok(serde_json::to_string_pretty( - &json!({"satoshi": wallet.balance()}), - )?) - } + let result: BalanceResult = balance.into(); + result.format(pretty) } CreateTx { @@ -319,17 +150,8 @@ pub fn handle_offline_wallet_subcommand( let psbt = tx_builder.finish()?; - let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); - - if wallet_opts.verbose { - Ok(serde_json::to_string_pretty( - &json!({"psbt": psbt_base64, "details": psbt}), - )?) - } else { - Ok(serde_json::to_string_pretty( - &json!({"psbt": psbt_base64 }), - )?) - } + let result = PsbtResult::with_details(&psbt, wallet_opts.verbose); + result.format(pretty) } BumpFee { txid, @@ -365,62 +187,24 @@ pub fn handle_offline_wallet_subcommand( let psbt = tx_builder.finish()?; - let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); - - Ok(serde_json::to_string_pretty( - &json!({"psbt": psbt_base64 }), - )?) + let result = PsbtResult::with_details(&psbt, wallet_opts.verbose); + result.format(pretty) } Policies => { let external_policy = wallet.policies(KeychainKind::External)?; let internal_policy = wallet.policies(KeychainKind::Internal)?; - if cli_opts.pretty { - let table = vec![ - vec![ - "External".cell().bold(true), - serde_json::to_string_pretty(&external_policy)?.cell(), - ], - vec![ - "Internal".cell().bold(true), - serde_json::to_string_pretty(&internal_policy)?.cell(), - ], - ] - .table() - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else { - Ok(serde_json::to_string_pretty(&json!({ - "external": external_policy, - "internal": internal_policy, - }))?) - } + let result = PoliciesResult { + external: serde_json::to_value(&external_policy).unwrap_or(json!(null)), + internal: serde_json::to_value(&internal_policy).unwrap_or(json!(null)), + }; + result.format(pretty) } PublicDescriptor => { - let external = wallet.public_descriptor(KeychainKind::External).to_string(); - let internal = wallet.public_descriptor(KeychainKind::Internal).to_string(); - - if cli_opts.pretty { - let table = vec![ - vec![ - "External Descriptor".cell().bold(true), - external.to_string().cell(), - ], - vec![ - "Internal Descriptor".cell().bold(true), - internal.to_string().cell(), - ], - ] - .table() - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else { - Ok(serde_json::to_string_pretty(&json!({ - "external": external.to_string(), - "internal": internal.to_string(), - }))?) - } + let result = PublicDescriptorResult { + external: wallet.public_descriptor(KeychainKind::External).to_string(), + internal: wallet.public_descriptor(KeychainKind::Internal).to_string(), + }; + result.format(pretty) } Sign { psbt, @@ -435,35 +219,16 @@ pub fn handle_offline_wallet_subcommand( ..Default::default() }; let finalized = wallet.sign(&mut psbt, signopt)?; - let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); - if wallet_opts.verbose { - Ok(serde_json::to_string_pretty( - &json!({"psbt": &psbt_base64, "is_finalized": finalized, "serialized_psbt": &psbt}), - )?) - } else { - Ok(serde_json::to_string_pretty( - &json!({"psbt": &psbt_base64, "is_finalized": finalized}), - )?) - } + + let result = PsbtResult::with_status_and_details(&psbt, finalized, wallet_opts.verbose); + result.format(pretty) } ExtractPsbt { psbt } => { let psbt_serialized = BASE64_STANDARD.decode(psbt)?; let psbt = Psbt::deserialize(&psbt_serialized)?; let raw_tx = psbt.extract_tx()?; - if cli_opts.pretty { - let table = vec![vec![ - "Raw Transaction".cell().bold(true), - serialize_hex(&raw_tx).cell(), - ]] - .table() - .display() - .map_err(|e| Error::Generic(e.to_string()))?; - Ok(format!("{table}")) - } else { - Ok(serde_json::to_string_pretty( - &json!({"raw_tx": serialize_hex(&raw_tx)}), - )?) - } + let result = RawPsbt::new(&raw_tx); + result.format(pretty) } FinalizePsbt { psbt, @@ -479,15 +244,9 @@ pub fn handle_offline_wallet_subcommand( ..Default::default() }; let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; - if wallet_opts.verbose { - Ok(serde_json::to_string_pretty( - &json!({ "psbt": BASE64_STANDARD.encode(psbt.serialize()), "is_finalized": finalized, "details": psbt}), - )?) - } else { - Ok(serde_json::to_string_pretty( - &json!({ "psbt": BASE64_STANDARD.encode(psbt.serialize()), "is_finalized": finalized}), - )?) - } + + let result = PsbtResult::with_status_and_details(&psbt, finalized, wallet_opts.verbose); + result.format(pretty) } CombinePsbt { psbt } => { let mut psbts = psbt @@ -508,9 +267,8 @@ pub fn handle_offline_wallet_subcommand( Ok(acc) }, )?; - Ok(serde_json::to_string_pretty( - &json!({ "psbt": BASE64_STANDARD.encode(final_psbt.serialize()) }), - )?) + let result = PsbtResult::new(&final_psbt); + result.format(pretty) } } } diff --git a/src/handlers/types.rs b/src/handlers/types.rs new file mode 100644 index 0000000..cd25313 --- /dev/null +++ b/src/handlers/types.rs @@ -0,0 +1,411 @@ +use crate::utils::output::FormatOutput; +use crate::{error::BDKCliError as Error, utils::shorten}; +use bdk_wallet::bitcoin::base64::Engine; +use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD; +use bdk_wallet::bitcoin::consensus::encode::serialize_hex; +use bdk_wallet::bitcoin::{Address, Network, Psbt, Transaction}; +use bdk_wallet::chain::ChainPosition; +use bdk_wallet::{AddressInfo, Balance, LocalOutput}; +use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; +use serde::Serialize; +use serde_json::json; + +/// Represent address result +#[derive(Serialize)] +pub struct AddressResult { + pub address: String, + pub index: u32, +} + +impl From for AddressResult { + fn from(info: AddressInfo) -> Self { + Self { + address: info.address.to_string(), + index: info.index, + } + } +} + +/// pretty presentation for address +impl FormatOutput for AddressResult { + fn to_table(&self) -> Result { + let table = vec![ + vec!["Address".cell().bold(true), self.address.clone().cell()], + vec!["Index".cell().bold(true), self.index.cell()], + ] + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} + +/// Represents the data for a single transaction +#[derive(Serialize)] +pub struct TransactionDetails { + pub txid: String, + pub is_coinbase: bool, + pub wtxid: String, + pub version: serde_json::Value, + pub is_rbf: bool, + pub inputs: serde_json::Value, + pub outputs: serde_json::Value, + #[serde(skip)] + pub version_display: String, + #[serde(skip)] + pub input_count: usize, + #[serde(skip)] + pub output_count: usize, + #[serde(skip)] + pub total_value: u64, +} + +/// A wrapper type for a list of transactions. +#[derive(Serialize)] +#[serde(transparent)] +pub struct TransactionListResult(pub Vec); + +impl FormatOutput for TransactionListResult { + fn to_table(&self) -> Result { + let mut rows: Vec> = vec![]; + + for tx in &self.0 { + rows.push(vec![ + tx.txid.clone().cell(), + tx.version_display.clone().cell().justify(Justify::Right), + tx.is_rbf.to_string().cell().justify(Justify::Center), + tx.input_count.to_string().cell().justify(Justify::Right), + tx.output_count.to_string().cell().justify(Justify::Right), + tx.total_value.to_string().cell().justify(Justify::Right), + ]); + } + + let table = rows + .table() + .title(vec![ + "Txid".cell().bold(true), + "Version".cell().bold(true), + "Is RBF".cell().bold(true), + "Input Count".cell().bold(true), + "Output Count".cell().bold(true), + "Total Value (sat)".cell().bold(true), + ]) + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} + +/// Balance representation +#[derive(Serialize)] +pub struct BalanceResult { + pub total: u64, + pub trusted_pending: u64, + pub untrusted_pending: u64, + pub immature: u64, + pub confirmed: u64, +} + +impl From for BalanceResult { + fn from(b: Balance) -> Self { + Self { + total: b.total().to_sat(), + confirmed: b.confirmed.to_sat(), + trusted_pending: b.trusted_pending.to_sat(), + untrusted_pending: b.untrusted_pending.to_sat(), + immature: b.immature.to_sat(), + } + } +} + +impl FormatOutput for BalanceResult { + fn to_table(&self) -> Result { + let table = vec![ + vec![ + "Total".cell().bold(true), + self.total.cell().justify(Justify::Right), + ], + vec![ + "Confirmed".cell().bold(true), + self.confirmed.cell().justify(Justify::Right), + ], + vec![ + "Trusted Pending".cell().bold(true), + self.trusted_pending.cell().justify(Justify::Right), + ], + vec![ + "Untrusted Pending".cell().bold(true), + self.untrusted_pending.cell().justify(Justify::Right), + ], + vec![ + "Immature".cell().bold(true), + self.immature.cell().justify(Justify::Right), + ], + ] + .table() + .title(vec![ + "Status".cell().bold(true), + "Amount (sat)".cell().bold(true), + ]) + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} + +/// single UTXO +#[derive(Serialize)] +pub struct UnspentDetails { + pub outpoint: String, + pub txout: serde_json::Value, + pub keychain: String, + pub is_spent: bool, + pub derivation_index: u32, + pub chain_position: serde_json::Value, + + #[serde(skip)] + pub value_sat: u64, + #[serde(skip)] + pub address: String, + #[serde(skip)] + pub outpoint_display: String, + #[serde(skip)] + pub height_display: String, + #[serde(skip)] + pub block_hash_display: String, +} + +impl UnspentDetails { + pub fn from_local_output(utxo: &LocalOutput, network: Network) -> Self { + let height = utxo.chain_position.confirmation_height_upper_bound(); + let height_display = height + .map(|h| h.to_string()) + .unwrap_or_else(|| "Pending".to_string()); + + let (_, block_hash_display) = match &utxo.chain_position { + ChainPosition::Confirmed { anchor, .. } => { + let hash = anchor.block_id.hash.to_string(); + (Some(hash.clone()), shorten(&hash, 8, 8)) + } + ChainPosition::Unconfirmed { .. } => (None, "Unconfirmed".to_string()), + }; + + let address = Address::from_script(&utxo.txout.script_pubkey, network) + .map(|a| a.to_string()) + .unwrap_or_else(|_| "Unknown Script".to_string()); + + let outpoint_str = utxo.outpoint.to_string(); + + Self { + outpoint: outpoint_str.clone(), + txout: serde_json::to_value(&utxo.txout).unwrap_or(json!({})), + keychain: format!("{:?}", utxo.keychain), + is_spent: utxo.is_spent, + derivation_index: utxo.derivation_index, + chain_position: serde_json::to_value(utxo.chain_position).unwrap_or(json!({})), + + value_sat: utxo.txout.value.to_sat(), + address, + outpoint_display: shorten(&outpoint_str, 8, 10), + height_display, + block_hash_display, + } + } +} + +/// Wrapper for the list of UTXOs +#[derive(Serialize)] +#[serde(transparent)] +pub struct UnspentListResult(pub Vec); + +impl FormatOutput for UnspentListResult { + fn to_table(&self) -> Result { + let mut rows: Vec> = vec![]; + + for utxo in &self.0 { + rows.push(vec![ + utxo.outpoint_display.clone().cell(), + utxo.value_sat.to_string().cell().justify(Justify::Right), + utxo.address.clone().cell(), + utxo.keychain.clone().cell(), + utxo.is_spent.cell(), + utxo.derivation_index.cell(), + utxo.height_display.clone().cell().justify(Justify::Right), + utxo.block_hash_display + .clone() + .cell() + .justify(Justify::Right), + ]); + } + + let table = rows + .table() + .title(vec![ + "Outpoint".cell().bold(true), + "Output (sat)".cell().bold(true), + "Output Address".cell().bold(true), + "Keychain".cell().bold(true), + "Is Spent".cell().bold(true), + "Index".cell().bold(true), + "Block Height".cell().bold(true), + "Block Hash".cell().bold(true), + ]) + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} + +#[derive(Serialize)] +pub struct PsbtResult { + pub psbt: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_finalized: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl PsbtResult { + pub fn new(psbt: &Psbt) -> Self { + Self { + psbt: BASE64_STANDARD.encode(psbt.serialize()), + is_finalized: None, + details: None, + } + } + + pub fn with_details(psbt: &Psbt, verbose: bool) -> Self { + Self { + psbt: BASE64_STANDARD.encode(psbt.serialize()), + is_finalized: None, + details: if verbose { + Some(serde_json::to_value(psbt).unwrap_or(json!({}))) + } else { + None + }, + } + } + + pub fn with_status_and_details(psbt: &Psbt, is_finalized: bool, verbose: bool) -> Self { + Self { + psbt: BASE64_STANDARD.encode(psbt.serialize()), + is_finalized: Some(is_finalized), + details: if verbose { + Some(serde_json::to_value(psbt).unwrap_or(json!({}))) + } else { + None + }, + } + } +} + +impl FormatOutput for PsbtResult { + fn to_table(&self) -> Result { + let mut rows = vec![vec![ + "PSBT (Base64)".cell().bold(true), + self.psbt.clone().cell(), + ]]; + + if let Some(finalized) = self.is_finalized { + rows.push(vec!["Is Finalized".cell().bold(true), finalized.cell()]); + } + + if self.details.is_some() { + rows.push(vec![ + "Details".cell().bold(true), + "Run without --pretty to view verbose JSON details".cell(), + ]); + } + + let table = rows + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(format!("{table}")) + } +} + +/// Policies representation +#[derive(Serialize)] +pub struct PoliciesResult { + pub external: serde_json::Value, + pub internal: serde_json::Value, +} + +impl FormatOutput for PoliciesResult { + fn to_table(&self) -> Result { + let ext_str = serde_json::to_string_pretty(&self.external) + .map_err(|e| Error::Generic(e.to_string()))?; + let int_str = serde_json::to_string_pretty(&self.internal) + .map_err(|e| Error::Generic(e.to_string()))?; + + let table = vec![ + vec!["External".cell().bold(true), ext_str.cell()], + vec!["Internal".cell().bold(true), int_str.cell()], + ] + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} + +#[derive(Serialize)] +pub struct PublicDescriptorResult { + pub external: String, + pub internal: String, +} + +impl FormatOutput for PublicDescriptorResult { + fn to_table(&self) -> Result { + let table = vec![ + vec![ + "External Descriptor".cell().bold(true), + self.external.clone().cell(), + ], + vec![ + "Internal Descriptor".cell().bold(true), + self.internal.clone().cell(), + ], + ] + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} + +#[derive(Serialize)] +pub struct RawPsbt { + pub raw_tx: String, +} + +impl RawPsbt { + pub fn new(tx: &Transaction) -> Self { + Self { + raw_tx: serialize_hex(tx), + } + } +} + +impl FormatOutput for RawPsbt { + fn to_table(&self) -> Result { + let table = vec![vec![ + "Raw Transaction".cell().bold(true), + self.raw_tx.clone().cell(), + ]] + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0e827e4..cea8935 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,4 @@ pub mod common; pub mod descriptors; - +pub mod output; pub use common::*; diff --git a/src/utils/output.rs b/src/utils/output.rs new file mode 100644 index 0000000..96b8d9e --- /dev/null +++ b/src/utils/output.rs @@ -0,0 +1,17 @@ +use crate::error::BDKCliError as Error; +use serde::Serialize; + +/// A trait for data structures that can be rendered to the CLI. +pub trait FormatOutput: Serialize { + /// Implement this to define how the data looks as a CLI table. + fn to_table(&self) -> Result; + + /// Formats the output based on the user's `--pretty` flag. + fn format(&self, pretty: bool) -> Result { + if pretty { + self.to_table() + } else { + serde_json::to_string_pretty(self).map_err(|e| Error::Generic(e.to_string())) + } + } +}