From 990f30a7da5e840f3b2d1e68a94dc45235f7874f Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Wed, 29 Apr 2026 09:11:33 +0100 Subject: [PATCH] ref(types): add simple table helper - add simple table helper fn for creating tables - refactor types to use simple table helper - add type defs for key, wallets and descriptors --- src/handlers/types.rs | 269 ++++++++++++++++++++++++------------------ src/utils/output.rs | 23 +++- 2 files changed, 172 insertions(+), 120 deletions(-) diff --git a/src/handlers/types.rs b/src/handlers/types.rs index cd25313..94aca12 100644 --- a/src/handlers/types.rs +++ b/src/handlers/types.rs @@ -1,11 +1,12 @@ -use crate::utils::output::FormatOutput; +use std::collections::HashMap; + +use crate::config::WalletConfigInner; +use crate::utils::output::{FormatOutput, simple_table}; 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 bdk_wallet::bitcoin::{ + Address, Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex, +}; +use bdk_wallet::{AddressInfo, Balance, LocalOutput, chain::ChainPosition}; use cli_table::{Cell, CellStruct, Style, Table, format::Justify}; use serde::Serialize; use serde_json::json; @@ -29,15 +30,16 @@ impl From for AddressResult { /// 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}")) + simple_table( + vec![ + vec!["Address".cell().bold(true), self.address.clone().cell()], + vec![ + "Index".cell().bold(true), + self.index.cell().justify(Justify::Right), + ], + ], + None, + ) } } @@ -122,37 +124,34 @@ impl From for BalanceResult { 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), - ], + simple_table( vec![ - "Immature".cell().bold(true), - self.immature.cell().justify(Justify::Right), + 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}")) + Some(vec![ + "Status".cell().bold(true), + "Amount (sat)".cell().bold(true), + ]), + ) } } @@ -272,32 +271,12 @@ pub struct PsbtResult { } 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 { + pub fn new(psbt: &Psbt, verbose: bool, finalized: Option) -> Self { Self { - psbt: BASE64_STANDARD.encode(psbt.serialize()), - is_finalized: Some(is_finalized), + psbt: bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD.encode(psbt.serialize()), + is_finalized: finalized, details: if verbose { - Some(serde_json::to_value(psbt).unwrap_or(json!({}))) + Some(serde_json::to_value(psbt).unwrap_or_default()) } else { None }, @@ -331,24 +310,25 @@ impl FormatOutput for PsbtResult { } } -/// Policies representation #[derive(Serialize)] -pub struct PoliciesResult { - pub external: serde_json::Value, - pub internal: serde_json::Value, +pub struct RawPsbt { + pub raw_tx: String, } -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()))?; +impl RawPsbt { + pub fn new(tx: &Transaction) -> Self { + Self { + raw_tx: serialize_hex(tx), + } + } +} - let table = vec![ - vec!["External".cell().bold(true), ext_str.cell()], - vec!["Internal".cell().bold(true), int_str.cell()], - ] +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()))?; @@ -358,54 +338,109 @@ impl FormatOutput for PoliciesResult { } #[derive(Serialize)] -pub struct PublicDescriptorResult { - pub external: String, - pub internal: String, +pub struct KeychainPair { + pub external: T, + pub internal: T, } -impl FormatOutput for PublicDescriptorResult { +// Table formatting for string pairs (used by PublicDescriptor) +impl FormatOutput for KeychainPair { 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}")) + let rows = vec![ + vec!["External".cell().bold(true), self.external.clone().cell()], + vec!["Internal".cell().bold(true), self.internal.clone().cell()], + ]; + simple_table(rows, None) } } +// Table formatting for JSON value pairs (used by Policies) +impl FormatOutput for KeychainPair { + 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 rows = vec![ + vec!["External".cell().bold(true), ext_str.cell()], + vec!["Internal".cell().bold(true), int_str.cell()], + ]; + simple_table(rows, None) + } +} #[derive(Serialize)] -pub struct RawPsbt { - pub raw_tx: String, +pub struct KeyResult { + pub xprv: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub xpub: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub mnemonic: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub fingerprint: Option, } -impl RawPsbt { - pub fn new(tx: &Transaction) -> Self { - Self { - raw_tx: serialize_hex(tx), +impl FormatOutput for KeyResult { + fn to_table(&self) -> Result { + let mut rows: Vec> = vec![]; + + if let Some(mnemonic) = &self.mnemonic { + rows.push(vec!["Mnemonic".cell().bold(true), mnemonic.clone().cell()]); } + if let Some(xpub) = &self.xpub { + rows.push(vec!["Xpub".cell().bold(true), xpub.clone().cell()]); + } + + rows.push(vec!["Xprv".cell().bold(true), self.xprv.clone().cell()]); + + if let Some(fingerprint) = &self.fingerprint { + rows.push(vec![ + "Fingerprint".cell().bold(true), + fingerprint.clone().cell(), + ]); + } + + simple_table(rows, None) } } -impl FormatOutput for RawPsbt { + +#[derive(Serialize)] +#[serde(transparent)] +pub struct WalletsListResult(pub HashMap); + +impl FormatOutput for WalletsListResult { 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()))?; + if self.0.is_empty() { + return Ok("No wallets configured yet.".to_string()); + } - Ok(format!("{table}")) + let mut rows: Vec> = vec![]; + for (name, inner) in &self.0 { + let desc: String = inner.ext_descriptor.chars().take(30).collect(); + let desc_display = if inner.ext_descriptor.len() > 30 { + format!("{}...", desc) + } else { + desc + }; + + rows.push(vec![ + name.clone().cell(), + inner.network.clone().cell(), + desc_display.cell(), + ]); + } + + simple_table( + rows, + Some(vec![ + "Wallet Name".cell().bold(true), + "Network".cell().bold(true), + "External Descriptor".cell().bold(true), + ]), + ) } } diff --git a/src/utils/output.rs b/src/utils/output.rs index 96b8d9e..5c4360b 100644 --- a/src/utils/output.rs +++ b/src/utils/output.rs @@ -1,9 +1,10 @@ use crate::error::BDKCliError as Error; +use cli_table::{CellStruct, Table}; use serde::Serialize; -/// A trait for data structures that can be rendered to the CLI. +/// A trait for types that can be presented to the user. pub trait FormatOutput: Serialize { - /// Implement this to define how the data looks as a CLI table. + /// Return a pretty table representation. fn to_table(&self) -> Result; /// Formats the output based on the user's `--pretty` flag. @@ -11,7 +12,23 @@ pub trait FormatOutput: Serialize { if pretty { self.to_table() } else { - serde_json::to_string_pretty(self).map_err(|e| Error::Generic(e.to_string())) + serde_json::to_string_pretty(self) + .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}"))) } } } + +/// Helper for building simple tables +pub fn simple_table( + rows: Vec>, + title: Option>, +) -> Result { + let mut table = rows.table(); + if let Some(title) = title { + table = table.title(title); + } + table + .display() + .map_err(|e| Error::Generic(e.to_string())) + .map(|t| t.to_string()) +} -- 2.49.0