From: Vihiga Tyonum Date: Tue, 21 Apr 2026 10:19:03 +0000 (+0100) Subject: ref(handlers):mv handler fns into offline & others X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/FromScriptException.WitnessProgram.html?a=commitdiff_plain;h=bf0551b71c5da8cce5b3200f47202dd787e46a65;p=bdk-cli ref(handlers):mv handler fns into offline & others - move handler fns into offline, online, wallets modules --- diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs new file mode 100644 index 0000000..9d564eb --- /dev/null +++ b/src/handlers/offline.rs @@ -0,0 +1,516 @@ +use crate::commands::OfflineWalletSubCommand::*; +use crate::commands::{CliOpts, OfflineWalletSubCommand, WalletOpts}; +use crate::error::BDKCliError as Error; +use crate::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::script::PushBytesBuf; +use bdk_wallet::bitcoin::{Address, Amount, FeeRate, Psbt, Sequence, Txid}; +use bdk_wallet::chain::ChainPosition; +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; + +/// Execute an offline wallet sub-command +/// +/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. +pub fn handle_offline_wallet_subcommand( + wallet: &mut Wallet, + wallet_opts: &WalletOpts, + cli_opts: &CliOpts, + offline_subcommand: OfflineWalletSubCommand, +) -> Result { + 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, + }))?) + } + } + 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, + }))?) + } + } + 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 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)?) + } + } + 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)?) + } + } + 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()}), + )?) + } + } + + CreateTx { + recipients, + send_all, + enable_rbf, + offline_signer, + utxos, + unspendable, + fee_rate, + external_policy, + internal_policy, + add_data, + add_string, + } => { + let mut tx_builder = wallet.build_tx(); + + if send_all { + tx_builder.drain_wallet().drain_to(recipients[0].0.clone()); + } else { + let recipients = recipients + .into_iter() + .map(|(script, amount)| (script, Amount::from_sat(amount))) + .collect(); + tx_builder.set_recipients(recipients); + } + + if !enable_rbf { + tx_builder.set_exact_sequence(Sequence::MAX); + } + + if offline_signer { + tx_builder.include_output_redeem_witness_script(); + } + + if let Some(fee_rate) = fee_rate + && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) + { + tx_builder.fee_rate(fee_rate); + } + + if let Some(utxos) = utxos { + tx_builder.add_utxos(&utxos[..]).unwrap(); + } + + if let Some(unspendable) = unspendable { + tx_builder.unspendable(unspendable); + } + + if let Some(base64_data) = add_data { + let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); + tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); + } else if let Some(string_data) = add_string { + let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); + tx_builder.add_data(&data); + } + + let policies = vec![ + external_policy.map(|p| (p, KeychainKind::External)), + internal_policy.map(|p| (p, KeychainKind::Internal)), + ]; + + for (policy, keychain) in policies.into_iter().flatten() { + let policy = serde_json::from_str::>>(&policy)?; + tx_builder.policy_path(policy, keychain); + } + + 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 }), + )?) + } + } + BumpFee { + txid, + shrink_address, + offline_signer, + utxos, + unspendable, + fee_rate, + } => { + let txid = Txid::from_str(txid.as_str())?; + + let mut tx_builder = wallet.build_fee_bump(txid)?; + let fee_rate = + FeeRate::from_sat_per_vb(fee_rate as u64).unwrap_or(FeeRate::BROADCAST_MIN); + tx_builder.fee_rate(fee_rate); + + if let Some(address) = shrink_address { + let script_pubkey = address.script_pubkey(); + tx_builder.drain_to(script_pubkey); + } + + if offline_signer { + tx_builder.include_output_redeem_witness_script(); + } + + if let Some(utxos) = utxos { + tx_builder.add_utxos(&utxos[..]).unwrap(); + } + + if let Some(unspendable) = unspendable { + tx_builder.unspendable(unspendable); + } + + let psbt = tx_builder.finish()?; + + let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); + + Ok(serde_json::to_string_pretty( + &json!({"psbt": psbt_base64 }), + )?) + } + 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, + }))?) + } + } + 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(), + }))?) + } + } + Sign { + psbt, + assume_height, + trust_witness_utxo, + } => { + let psbt_bytes = BASE64_STANDARD.decode(psbt)?; + let mut psbt = Psbt::deserialize(&psbt_bytes)?; + let signopt = SignOptions { + assume_height, + trust_witness_utxo: trust_witness_utxo.unwrap_or(false), + ..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}), + )?) + } + } + 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)}), + )?) + } + } + FinalizePsbt { + psbt, + assume_height, + trust_witness_utxo, + } => { + let psbt_bytes = BASE64_STANDARD.decode(psbt)?; + let mut psbt: Psbt = Psbt::deserialize(&psbt_bytes)?; + + let signopt = SignOptions { + assume_height, + trust_witness_utxo: trust_witness_utxo.unwrap_or(false), + ..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}), + )?) + } + } + CombinePsbt { psbt } => { + let mut psbts = psbt + .iter() + .map(|s| { + let psbt = BASE64_STANDARD.decode(s)?; + Ok(Psbt::deserialize(&psbt)?) + }) + .collect::, Error>>()?; + + let init_psbt = psbts + .pop() + .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?; + let final_psbt = psbts.into_iter().try_fold::<_, _, Result>( + init_psbt, + |mut acc, x| { + let _ = acc.combine(x); + Ok(acc) + }, + )?; + Ok(serde_json::to_string_pretty( + &json!({ "psbt": BASE64_STANDARD.encode(final_psbt.serialize()) }), + )?) + } + } +} diff --git a/src/handlers/online.rs b/src/handlers/online.rs new file mode 100644 index 0000000..ec3ff2d --- /dev/null +++ b/src/handlers/online.rs @@ -0,0 +1,327 @@ +#[cfg(feature = "electrum")] +use crate::backend::BlockchainClient::Electrum; +#[cfg(feature = "cbf")] +use crate::backend::BlockchainClient::KyotoClient; +#[cfg(feature = "rpc")] +use crate::backend::BlockchainClient::RpcClient; +#[cfg(feature = "esplora")] +use {crate::backend::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; +#[cfg(feature = "rpc")] +use { + bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXS, bitcoincore_rpc::RpcApi}, + bdk_wallet::chain::{BlockId, CanonicalizationParams, CheckPoint}, +}; +#[cfg(any(feature = "electrum", feature = "esplora"))] +use {bdk_wallet::KeychainKind, std::collections::HashSet, std::io::Write}; + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +use { + crate::backend::{BlockchainClient, sync_kyoto_client}, + crate::commands::OnlineWalletSubCommand::*, + crate::error::BDKCliError as Error, + crate::payjoin::PayjoinManager, + crate::payjoin::ohttp::RelayManager, + crate::utils::is_final, + bdk_wallet::Wallet, + bdk_wallet::bitcoin::{ + Psbt, Transaction, Txid, base64::Engine, base64::prelude::BASE64_STANDARD, + consensus::Decodable, hex::FromHex, + }, + serde_json::json, + std::sync::{Arc, Mutex}, +}; + +/// Execute an online wallet sub-command +/// +/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`]. +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +pub(crate) async fn handle_online_wallet_subcommand( + wallet: &mut Wallet, + client: &BlockchainClient, + online_subcommand: crate::commands::OnlineWalletSubCommand, +) -> Result { + match online_subcommand { + FullScan { + stop_gap: _stop_gap, + } => { + #[cfg(any(feature = "electrum", feature = "esplora"))] + let request = wallet.start_full_scan().inspect({ + let mut stdout = std::io::stdout(); + let mut once = HashSet::::new(); + move |k, spk_i, _| { + if once.insert(k) { + print!("\nScanning keychain [{k:?}]"); + } + print!(" {spk_i:<3}"); + stdout.flush().expect("must flush"); + } + }); + match client { + #[cfg(feature = "electrum")] + Electrum { client, batch_size } => { + // Populate the electrum client's transaction cache so it doesn't re-download transaction we + // already have. + client + .populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); + + let update = client.full_scan(request, _stop_gap, *batch_size, false)?; + wallet.apply_update(update)?; + } + #[cfg(feature = "esplora")] + Esplora { + client, + parallel_requests, + } => { + let update = client + .full_scan(request, _stop_gap, *parallel_requests) + .await + .map_err(|e| *e)?; + wallet.apply_update(update)?; + } + + #[cfg(feature = "rpc")] + RpcClient { client } => { + let blockchain_info = client.get_blockchain_info()?; + + let genesis_block = + bdk_wallet::bitcoin::constants::genesis_block(wallet.network()); + let genesis_cp = CheckPoint::new(BlockId { + height: 0, + hash: genesis_block.block_hash(), + }); + let mut emitter = Emitter::new( + client.as_ref(), + genesis_cp.clone(), + genesis_cp.height(), + NO_EXPECTED_MEMPOOL_TXS, + ); + + while let Some(block_event) = emitter.next_block()? { + if block_event.block_height() % 10_000 == 0 { + let percent_done = f64::from(block_event.block_height()) + / f64::from(blockchain_info.headers as u32) + * 100f64; + println!( + "Applying block at height: {}, {:.2}% done.", + block_event.block_height(), + percent_done + ); + } + + wallet.apply_block_connected_to( + &block_event.block, + block_event.block_height(), + block_event.connected_to(), + )?; + } + + let mempool_txs = emitter.mempool()?; + wallet.apply_unconfirmed_txs(mempool_txs.update); + } + #[cfg(feature = "cbf")] + KyotoClient { client } => { + sync_kyoto_client(wallet, client).await?; + } + } + Ok(serde_json::to_string_pretty(&json!({}))?) + } + Sync => { + sync_wallet(client, wallet).await?; + Ok(serde_json::to_string_pretty(&json!({}))?) + } + Broadcast { psbt, tx } => { + let tx = match (psbt, tx) { + (Some(psbt), None) => { + let psbt = BASE64_STANDARD + .decode(psbt) + .map_err(|e| Error::Generic(e.to_string()))?; + let psbt: Psbt = Psbt::deserialize(&psbt)?; + is_final(&psbt)?; + psbt.extract_tx()? + } + (None, Some(tx)) => { + let tx_bytes = Vec::::from_hex(&tx)?; + Transaction::consensus_decode(&mut tx_bytes.as_slice())? + } + (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"), + (None, None) => panic!("Missing `psbt` and `tx` option"), + }; + let txid = broadcast_transaction(client, tx).await?; + Ok(serde_json::to_string_pretty(&json!({ "txid": txid }))?) + } + ReceivePayjoin { + amount, + directory, + ohttp_relay, + max_fee_rate, + } => { + let relay_manager = Arc::new(Mutex::new(RelayManager::new())); + let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager); + return payjoin_manager + .receive_payjoin(amount, directory, max_fee_rate, ohttp_relay, client) + .await; + } + SendPayjoin { + uri, + ohttp_relay, + fee_rate, + } => { + let relay_manager = Arc::new(Mutex::new(RelayManager::new())); + let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager); + return payjoin_manager + .send_payjoin(uri, fee_rate, ohttp_relay, client) + .await; + } + } +} + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +/// Syncs a given wallet using the blockchain client. +pub async fn sync_wallet(client: &BlockchainClient, wallet: &mut Wallet) -> Result<(), Error> { + // #[cfg(any(feature = "electrum", feature = "esplora"))] + let request = wallet + .start_sync_with_revealed_spks() + .inspect(|item, progress| { + let pc = (100 * progress.consumed()) as f32 / progress.total() as f32; + eprintln!("[ SCANNING {pc:03.0}% ] {item}"); + }); + match client { + #[cfg(feature = "electrum")] + Electrum { client, batch_size } => { + // Populate the electrum client's transaction cache so it doesn't re-download transaction we + // already have. + client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); + + let update = client.sync(request, *batch_size, false)?; + wallet + .apply_update(update) + .map_err(|e| Error::Generic(e.to_string())) + } + #[cfg(feature = "esplora")] + Esplora { + client, + parallel_requests, + } => { + let update = client + .sync(request, *parallel_requests) + .await + .map_err(|e| *e)?; + wallet + .apply_update(update) + .map_err(|e| Error::Generic(e.to_string())) + } + #[cfg(feature = "rpc")] + RpcClient { client } => { + let blockchain_info = client.get_blockchain_info()?; + let wallet_cp = wallet.latest_checkpoint(); + + // reload the last 200 blocks in case of a reorg + let emitter_height = wallet_cp.height().saturating_sub(200); + let mut emitter = Emitter::new( + client.as_ref(), + wallet_cp, + emitter_height, + wallet + .tx_graph() + .list_canonical_txs( + wallet.local_chain(), + wallet.local_chain().tip().block_id(), + CanonicalizationParams::default(), + ) + .filter(|tx| tx.chain_position.is_unconfirmed()), + ); + + while let Some(block_event) = emitter.next_block()? { + if block_event.block_height() % 10_000 == 0 { + let percent_done = f64::from(block_event.block_height()) + / f64::from(blockchain_info.headers as u32) + * 100f64; + println!( + "Applying block at height: {}, {:.2}% done.", + block_event.block_height(), + percent_done + ); + } + + wallet.apply_block_connected_to( + &block_event.block, + block_event.block_height(), + block_event.connected_to(), + )?; + } + + let mempool_txs = emitter.mempool()?; + wallet.apply_unconfirmed_txs(mempool_txs.update); + Ok(()) + } + #[cfg(feature = "cbf")] + KyotoClient { client } => sync_kyoto_client(wallet, client) + .await + .map_err(|e| Error::Generic(e.to_string())), + } +} + +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +/// Broadcasts a given transaction using the blockchain client. +pub async fn broadcast_transaction( + client: &BlockchainClient, + tx: Transaction, +) -> Result { + match client { + #[cfg(feature = "electrum")] + Electrum { + client, + batch_size: _, + } => client + .transaction_broadcast(&tx) + .map_err(|e| Error::Generic(e.to_string())), + #[cfg(feature = "esplora")] + Esplora { + client, + parallel_requests: _, + } => client + .broadcast(&tx) + .await + .map(|()| tx.compute_txid()) + .map_err(|e| Error::Generic(e.to_string())), + #[cfg(feature = "rpc")] + RpcClient { client } => client + .send_raw_transaction(&tx) + .map_err(|e| Error::Generic(e.to_string())), + + #[cfg(feature = "cbf")] + KyotoClient { client } => { + let txid = tx.compute_txid(); + let wtxid = client + .requester + .broadcast_random(tx.clone()) + .await + .map_err(|_| { + tracing::warn!("Broadcast was unsuccessful"); + Error::Generic("Transaction broadcast timed out after 30 seconds".into()) + })?; + tracing::info!("Successfully broadcast WTXID: {wtxid}"); + Ok(txid) + } + } +} diff --git a/src/handlers/wallets.rs b/src/handlers/wallets.rs new file mode 100644 index 0000000..17c25c7 --- /dev/null +++ b/src/handlers/wallets.rs @@ -0,0 +1,37 @@ +use crate::config::WalletConfig; +use crate::error::BDKCliError as Error; +use cli_table::{Cell, CellStruct, Style, Table}; +use std::path::Path; + +/// Handle the top-level `wallets` command (lists all saved wallets) +pub fn handle_wallets_subcommand(home_dir: &Path, pretty: bool) -> Result { + let config = match WalletConfig::load(home_dir)? { + Some(cfg) => cfg, + None => return Ok("No wallets configured yet.".to_string()), + }; + + if pretty { + let mut rows: Vec> = vec![]; + for (name, inner) in &config.wallets { + rows.push(vec![ + name.cell(), + inner.network.clone().cell(), + inner.ext_descriptor[..30].to_string().cell(), + ]); + } + + let table = rows + .table() + .title(vec![ + "Wallet Name".cell().bold(true), + "Network".cell().bold(true), + "External Descriptor (truncated)".cell().bold(true), + ]) + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + + Ok(format!("{table}")) + } else { + Ok(serde_json::to_string_pretty(&config.wallets)?) + } +}