Skip to content

Commit 8fc06d8

Browse files
committed
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
1 parent 91d5a69 commit 8fc06d8

2 files changed

Lines changed: 172 additions & 120 deletions

File tree

src/handlers/types.rs

Lines changed: 152 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
use crate::utils::output::FormatOutput;
1+
use std::collections::HashMap;
2+
3+
use crate::config::WalletConfigInner;
4+
use crate::utils::output::{FormatOutput, simple_table};
25
use crate::{error::BDKCliError as Error, utils::shorten};
3-
use bdk_wallet::bitcoin::base64::Engine;
4-
use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD;
5-
use bdk_wallet::bitcoin::consensus::encode::serialize_hex;
6-
use bdk_wallet::bitcoin::{Address, Network, Psbt, Transaction};
7-
use bdk_wallet::chain::ChainPosition;
8-
use bdk_wallet::{AddressInfo, Balance, LocalOutput};
6+
use bdk_wallet::bitcoin::{
7+
Address, Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex,
8+
};
9+
use bdk_wallet::{AddressInfo, Balance, LocalOutput, chain::ChainPosition};
910
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
1011
use serde::Serialize;
1112
use serde_json::json;
@@ -29,15 +30,16 @@ impl From<AddressInfo> for AddressResult {
2930
/// pretty presentation for address
3031
impl FormatOutput for AddressResult {
3132
fn to_table(&self) -> Result<String, Error> {
32-
let table = vec![
33-
vec!["Address".cell().bold(true), self.address.clone().cell()],
34-
vec!["Index".cell().bold(true), self.index.cell()],
35-
]
36-
.table()
37-
.display()
38-
.map_err(|e| Error::Generic(e.to_string()))?;
39-
40-
Ok(format!("{table}"))
33+
simple_table(
34+
vec![
35+
vec!["Address".cell().bold(true), self.address.clone().cell()],
36+
vec![
37+
"Index".cell().bold(true),
38+
self.index.cell().justify(Justify::Right),
39+
],
40+
],
41+
None,
42+
)
4143
}
4244
}
4345

@@ -122,37 +124,34 @@ impl From<Balance> for BalanceResult {
122124

123125
impl FormatOutput for BalanceResult {
124126
fn to_table(&self) -> Result<String, Error> {
125-
let table = vec![
126-
vec![
127-
"Total".cell().bold(true),
128-
self.total.cell().justify(Justify::Right),
129-
],
130-
vec![
131-
"Confirmed".cell().bold(true),
132-
self.confirmed.cell().justify(Justify::Right),
133-
],
134-
vec![
135-
"Trusted Pending".cell().bold(true),
136-
self.trusted_pending.cell().justify(Justify::Right),
137-
],
138-
vec![
139-
"Untrusted Pending".cell().bold(true),
140-
self.untrusted_pending.cell().justify(Justify::Right),
141-
],
127+
simple_table(
142128
vec![
143-
"Immature".cell().bold(true),
144-
self.immature.cell().justify(Justify::Right),
129+
vec![
130+
"Total".cell().bold(true),
131+
self.total.cell().justify(Justify::Right),
132+
],
133+
vec![
134+
"Confirmed".cell().bold(true),
135+
self.confirmed.cell().justify(Justify::Right),
136+
],
137+
vec![
138+
"Trusted Pending".cell().bold(true),
139+
self.trusted_pending.cell().justify(Justify::Right),
140+
],
141+
vec![
142+
"Untrusted Pending".cell().bold(true),
143+
self.untrusted_pending.cell().justify(Justify::Right),
144+
],
145+
vec![
146+
"Immature".cell().bold(true),
147+
self.immature.cell().justify(Justify::Right),
148+
],
145149
],
146-
]
147-
.table()
148-
.title(vec![
149-
"Status".cell().bold(true),
150-
"Amount (sat)".cell().bold(true),
151-
])
152-
.display()
153-
.map_err(|e| Error::Generic(e.to_string()))?;
154-
155-
Ok(format!("{table}"))
150+
Some(vec![
151+
"Status".cell().bold(true),
152+
"Amount (sat)".cell().bold(true),
153+
]),
154+
)
156155
}
157156
}
158157

@@ -272,32 +271,12 @@ pub struct PsbtResult {
272271
}
273272

274273
impl PsbtResult {
275-
pub fn new(psbt: &Psbt) -> Self {
276-
Self {
277-
psbt: BASE64_STANDARD.encode(psbt.serialize()),
278-
is_finalized: None,
279-
details: None,
280-
}
281-
}
282-
283-
pub fn with_details(psbt: &Psbt, verbose: bool) -> Self {
284-
Self {
285-
psbt: BASE64_STANDARD.encode(psbt.serialize()),
286-
is_finalized: None,
287-
details: if verbose {
288-
Some(serde_json::to_value(psbt).unwrap_or(json!({})))
289-
} else {
290-
None
291-
},
292-
}
293-
}
294-
295-
pub fn with_status_and_details(psbt: &Psbt, is_finalized: bool, verbose: bool) -> Self {
274+
pub fn new(psbt: &Psbt, verbose: bool, finalized: Option<bool>) -> Self {
296275
Self {
297-
psbt: BASE64_STANDARD.encode(psbt.serialize()),
298-
is_finalized: Some(is_finalized),
276+
psbt: bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD.encode(psbt.serialize()),
277+
is_finalized: finalized,
299278
details: if verbose {
300-
Some(serde_json::to_value(psbt).unwrap_or(json!({})))
279+
Some(serde_json::to_value(psbt).unwrap_or_default())
301280
} else {
302281
None
303282
},
@@ -331,24 +310,25 @@ impl FormatOutput for PsbtResult {
331310
}
332311
}
333312

334-
/// Policies representation
335313
#[derive(Serialize)]
336-
pub struct PoliciesResult {
337-
pub external: serde_json::Value,
338-
pub internal: serde_json::Value,
314+
pub struct RawPsbt {
315+
pub raw_tx: String,
339316
}
340317

341-
impl FormatOutput for PoliciesResult {
342-
fn to_table(&self) -> Result<String, Error> {
343-
let ext_str = serde_json::to_string_pretty(&self.external)
344-
.map_err(|e| Error::Generic(e.to_string()))?;
345-
let int_str = serde_json::to_string_pretty(&self.internal)
346-
.map_err(|e| Error::Generic(e.to_string()))?;
318+
impl RawPsbt {
319+
pub fn new(tx: &Transaction) -> Self {
320+
Self {
321+
raw_tx: serialize_hex(tx),
322+
}
323+
}
324+
}
347325

348-
let table = vec![
349-
vec!["External".cell().bold(true), ext_str.cell()],
350-
vec!["Internal".cell().bold(true), int_str.cell()],
351-
]
326+
impl FormatOutput for RawPsbt {
327+
fn to_table(&self) -> Result<String, Error> {
328+
let table = vec![vec![
329+
"Raw Transaction".cell().bold(true),
330+
self.raw_tx.clone().cell(),
331+
]]
352332
.table()
353333
.display()
354334
.map_err(|e| Error::Generic(e.to_string()))?;
@@ -358,54 +338,109 @@ impl FormatOutput for PoliciesResult {
358338
}
359339

360340
#[derive(Serialize)]
361-
pub struct PublicDescriptorResult {
362-
pub external: String,
363-
pub internal: String,
341+
pub struct KeychainPair<T> {
342+
pub external: T,
343+
pub internal: T,
364344
}
365345

366-
impl FormatOutput for PublicDescriptorResult {
346+
// Table formatting for string pairs (used by PublicDescriptor)
347+
impl FormatOutput for KeychainPair<String> {
367348
fn to_table(&self) -> Result<String, Error> {
368-
let table = vec![
369-
vec![
370-
"External Descriptor".cell().bold(true),
371-
self.external.clone().cell(),
372-
],
373-
vec![
374-
"Internal Descriptor".cell().bold(true),
375-
self.internal.clone().cell(),
376-
],
377-
]
378-
.table()
379-
.display()
380-
.map_err(|e| Error::Generic(e.to_string()))?;
381-
382-
Ok(format!("{table}"))
349+
let rows = vec![
350+
vec!["External".cell().bold(true), self.external.clone().cell()],
351+
vec!["Internal".cell().bold(true), self.internal.clone().cell()],
352+
];
353+
simple_table(rows, None)
383354
}
384355
}
385356

357+
// Table formatting for JSON value pairs (used by Policies)
358+
impl FormatOutput for KeychainPair<serde_json::Value> {
359+
fn to_table(&self) -> Result<String, Error> {
360+
let ext_str = serde_json::to_string_pretty(&self.external)
361+
.map_err(|e| Error::Generic(e.to_string()))?;
362+
let int_str = serde_json::to_string_pretty(&self.internal)
363+
.map_err(|e| Error::Generic(e.to_string()))?;
364+
365+
let rows = vec![
366+
vec!["External".cell().bold(true), ext_str.cell()],
367+
vec!["Internal".cell().bold(true), int_str.cell()],
368+
];
369+
simple_table(rows, None)
370+
}
371+
}
386372
#[derive(Serialize)]
387-
pub struct RawPsbt {
388-
pub raw_tx: String,
373+
pub struct KeyResult {
374+
pub xprv: String,
375+
376+
#[serde(skip_serializing_if = "Option::is_none")]
377+
pub xpub: Option<String>,
378+
379+
#[serde(skip_serializing_if = "Option::is_none")]
380+
pub mnemonic: Option<String>,
381+
382+
#[serde(skip_serializing_if = "Option::is_none")]
383+
pub fingerprint: Option<String>,
389384
}
390385

391-
impl RawPsbt {
392-
pub fn new(tx: &Transaction) -> Self {
393-
Self {
394-
raw_tx: serialize_hex(tx),
386+
impl FormatOutput for KeyResult {
387+
fn to_table(&self) -> Result<String, Error> {
388+
let mut rows: Vec<Vec<CellStruct>> = vec![];
389+
390+
if let Some(mnemonic) = &self.mnemonic {
391+
rows.push(vec!["Mnemonic".cell().bold(true), mnemonic.clone().cell()]);
395392
}
393+
if let Some(xpub) = &self.xpub {
394+
rows.push(vec!["Xpub".cell().bold(true), xpub.clone().cell()]);
395+
}
396+
397+
rows.push(vec!["Xprv".cell().bold(true), self.xprv.clone().cell()]);
398+
399+
if let Some(fingerprint) = &self.fingerprint {
400+
rows.push(vec![
401+
"Fingerprint".cell().bold(true),
402+
fingerprint.clone().cell(),
403+
]);
404+
}
405+
406+
simple_table(rows, None)
396407
}
397408
}
398409

399-
impl FormatOutput for RawPsbt {
410+
411+
#[derive(Serialize)]
412+
#[serde(transparent)]
413+
pub struct WalletsListResult(pub HashMap<String, WalletConfigInner>);
414+
415+
impl FormatOutput for WalletsListResult {
400416
fn to_table(&self) -> Result<String, Error> {
401-
let table = vec![vec![
402-
"Raw Transaction".cell().bold(true),
403-
self.raw_tx.clone().cell(),
404-
]]
405-
.table()
406-
.display()
407-
.map_err(|e| Error::Generic(e.to_string()))?;
417+
if self.0.is_empty() {
418+
return Ok("No wallets configured yet.".to_string());
419+
}
408420

409-
Ok(format!("{table}"))
421+
let mut rows: Vec<Vec<CellStruct>> = vec![];
422+
for (name, inner) in &self.0 {
423+
let desc: String = inner.ext_descriptor.chars().take(30).collect();
424+
let desc_display = if inner.ext_descriptor.len() > 30 {
425+
format!("{}...", desc)
426+
} else {
427+
desc
428+
};
429+
430+
rows.push(vec![
431+
name.clone().cell(),
432+
inner.network.clone().cell(),
433+
desc_display.cell(),
434+
]);
435+
}
436+
437+
simple_table(
438+
rows,
439+
Some(vec![
440+
"Wallet Name".cell().bold(true),
441+
"Network".cell().bold(true),
442+
"External Descriptor".cell().bold(true),
443+
]),
444+
)
410445
}
411446
}

src/utils/output.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
use crate::error::BDKCliError as Error;
2+
use cli_table::{CellStruct, Table};
23
use serde::Serialize;
34

4-
/// A trait for data structures that can be rendered to the CLI.
5+
/// A trait for types that can be presented to the user.
56
pub trait FormatOutput: Serialize {
6-
/// Implement this to define how the data looks as a CLI table.
7+
/// Return a pretty table representation.
78
fn to_table(&self) -> Result<String, Error>;
89

910
/// Formats the output based on the user's `--pretty` flag.
1011
fn format(&self, pretty: bool) -> Result<String, Error> {
1112
if pretty {
1213
self.to_table()
1314
} else {
14-
serde_json::to_string_pretty(self).map_err(|e| Error::Generic(e.to_string()))
15+
serde_json::to_string_pretty(self)
16+
.map_err(|e| Error::Generic(format!("JSON serialization failed: {e}")))
1517
}
1618
}
1719
}
20+
21+
/// Helper for building simple tables
22+
pub fn simple_table(
23+
rows: Vec<Vec<CellStruct>>,
24+
title: Option<Vec<CellStruct>>,
25+
) -> Result<String, Error> {
26+
let mut table = rows.table();
27+
if let Some(title) = title {
28+
table = table.title(title);
29+
}
30+
table
31+
.display()
32+
.map_err(|e| Error::Generic(e.to_string()))
33+
.map(|t| t.to_string())
34+
}

0 commit comments

Comments
 (0)