+++ /dev/null
-// Copyright (c) 2020-2025 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! Command Handlers
-//!
-//! This module describes all the command handling logic used by bdk-cli.
-use crate::commands::OfflineWalletSubCommand::*;
-use crate::commands::*;
-use crate::config::{WalletConfig, WalletConfigInner};
-use crate::error::BDKCliError as Error;
-#[cfg(any(feature = "sqlite", feature = "redb"))]
-use crate::persister::Persister;
-#[cfg(feature = "cbf")]
-use crate::utils::BlockchainClient::KyotoClient;
-use crate::utils::*;
-#[cfg(feature = "redb")]
-use bdk_redb::Store as RedbStore;
-use bdk_wallet::bip39::{Language, Mnemonic};
-use bdk_wallet::bitcoin::base64::Engine;
-use bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD;
-use bdk_wallet::bitcoin::{
- Address, Amount, FeeRate, Network, Psbt, Sequence, Txid,
- bip32::{DerivationPath, KeySource},
- consensus::encode::serialize_hex,
- script::PushBytesBuf,
- secp256k1::Secp256k1,
-};
-use bdk_wallet::chain::ChainPosition;
-use bdk_wallet::descriptor::Segwitv0;
-use bdk_wallet::keys::{
- DerivableKey, DescriptorKey, DescriptorKey::Secret, ExtendedKey, GeneratableKey, GeneratedKey,
- bip39::WordCount,
-};
-use bdk_wallet::miniscript::miniscript;
-#[cfg(feature = "sqlite")]
-use bdk_wallet::rusqlite::Connection;
-use bdk_wallet::{KeychainKind, SignOptions, Wallet};
-#[cfg(feature = "compiler")]
-use bdk_wallet::{
- bitcoin::XOnlyPublicKey,
- descriptor::{Descriptor, Legacy, Miniscript},
- miniscript::{Tap, descriptor::TapTree, policy::Concrete},
-};
-use clap::CommandFactory;
-use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
-use serde_json::json;
-#[cfg(feature = "silent-payments")]
-use {
- bdk_sp::{
- bitcoin::{PrivateKey, PublicKey, ScriptBuf},
- encoding::SilentPaymentCode,
- send::psbt::derive_sp,
- },
- bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey},
-};
-
-#[cfg(feature = "electrum")]
-use crate::utils::BlockchainClient::Electrum;
-#[cfg(any(feature = "electrum", feature = "esplora"))]
-use std::collections::HashSet;
-use std::collections::{BTreeMap, HashMap};
-use std::convert::TryFrom;
-#[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))]
-use std::io::Write;
-use std::path::Path;
-use std::str::FromStr;
-#[cfg(any(
- feature = "redb",
- feature = "compiler",
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
-))]
-use std::sync::Arc;
-
-#[cfg(feature = "bip322")]
-use crate::error::BDKCliError;
-#[cfg(feature = "bip322")]
-use bdk_bip322::{BIP322, MessageProof, MessageVerificationResult};
-
-#[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
-))]
-use {
- crate::commands::OnlineWalletSubCommand::*,
- crate::payjoin::{PayjoinManager, ohttp::RelayManager},
- bdk_wallet::bitcoin::{Transaction, consensus::Decodable, hex::FromHex},
- std::sync::Mutex,
-};
-#[cfg(feature = "esplora")]
-use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
-#[cfg(feature = "rpc")]
-use {
- crate::utils::BlockchainClient::RpcClient,
- bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXS, bitcoincore_rpc::RpcApi},
- bdk_wallet::chain::{BlockId, CanonicalizationParams, CheckPoint},
-};
-
-#[cfg(feature = "compiler")]
-const NUMS_UNSPENDABLE_KEY_HEX: &str =
- "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
-
-/// 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<String, Error> {
- 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::<Vec<_>>();
- if cli_opts.pretty {
- let mut rows: Vec<Vec<CellStruct>> = 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::<u64>();
- (
- 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::<Vec<_>>();
- let mut rows: Vec<Vec<CellStruct>> = 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()}),
- )?)
- }
- }
- #[cfg(feature = "silent-payments")]
- CreateSpTx {
- recipients: maybe_recipients,
- silent_payment_recipients,
- send_all,
- offline_signer,
- utxos,
- unspendable,
- fee_rate,
- external_policy,
- internal_policy,
- add_data,
- add_string,
- } => {
- let mut tx_builder = wallet.build_tx();
-
- let sp_recipients: Vec<SilentPaymentCode> = silent_payment_recipients
- .iter()
- .map(|(sp_code, _)| sp_code.clone())
- .collect();
-
- if send_all {
- if sp_recipients.len() == 1 && maybe_recipients.is_none() {
- tx_builder
- .drain_wallet()
- .drain_to(sp_recipients[0].get_placeholder_p2tr_spk());
- } else if let Some(ref recipients) = maybe_recipients
- && sp_recipients.is_empty()
- {
- if recipients.len() == 1 {
- tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
- } else {
- return Err(Error::Generic(
- "Wallet can only be drain to a single output".to_string(),
- ));
- }
- } else {
- return Err(Error::Generic(
- "Wallet can only be drain to a single output".to_string(),
- ));
- }
- } else {
- let mut outputs: Vec<(ScriptBuf, Amount)> = silent_payment_recipients
- .iter()
- .map(|(sp_code, amount)| {
- let script = sp_code.get_placeholder_p2tr_spk();
- (script, Amount::from_sat(*amount))
- })
- .collect();
-
- if let Some(recipients) = maybe_recipients {
- let recipients = recipients
- .into_iter()
- .map(|(script, amount)| (script, Amount::from_sat(amount)));
-
- outputs.extend(recipients);
- }
-
- tx_builder.set_recipients(outputs);
- }
-
- // Do not enable RBF for this transaction
- 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[..])
- .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?;
- }
-
- 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)
- .map_err(|e| Error::Generic(e.to_string()))?;
- tx_builder.add_data(
- &PushBytesBuf::try_from(op_return_data)
- .map_err(|e| Error::Generic(e.to_string()))?,
- );
- } else if let Some(string_data) = add_string {
- let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec())
- .map_err(|e| Error::Generic(e.to_string()))?;
- 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::<BTreeMap<String, Vec<usize>>>(&policy)?;
- tx_builder.policy_path(policy, keychain);
- }
-
- let mut psbt = tx_builder.finish()?;
-
- let unsigned_psbt = psbt.clone();
-
- let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
-
- if !finalized {
- return Err(Error::Generic(
- "Cannot produce silent payment outputs without intermediate signing phase."
- .to_string(),
- ));
- }
-
- for (full_input, psbt_input) in unsigned_psbt.inputs.iter().zip(psbt.inputs.iter_mut())
- {
- // repopulate key derivation data
- psbt_input.bip32_derivation = full_input.bip32_derivation.clone();
- psbt_input.tap_key_origins = full_input.tap_key_origins.clone();
- }
-
- let secp = Secp256k1::new();
- let mut external_signers = wallet.get_signers(KeychainKind::External).as_key_map(&secp);
- let internal_signers = wallet.get_signers(KeychainKind::Internal).as_key_map(&secp);
- external_signers.extend(internal_signers);
-
- match external_signers.iter().next().expect("not empty") {
- (DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
- match single_pub.key {
- SinglePubKey::FullKey(pk) => {
- let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
- derive_sp(&mut psbt, &keys, &sp_recipients, &secp)
- .expect("will fix later");
- }
- SinglePubKey::XOnly(xonly) => {
- let keys: HashMap<bdk_sp::bitcoin::XOnlyPublicKey, PrivateKey> =
- [(xonly, prv.key)].into();
- derive_sp(&mut psbt, &keys, &sp_recipients, &secp)
- .expect("will fix later");
- }
- };
- }
- (_, DescriptorSecretKey::XPrv(k)) => {
- derive_sp(&mut psbt, &k.xkey, &sp_recipients, &secp).expect("will fix later");
- }
- _ => unimplemented!("multi xkey signer"),
- };
-
- // Unfinalize PSBT to resign
- for psbt_input in psbt.inputs.iter_mut() {
- psbt_input.final_script_sig = None;
- psbt_input.final_script_witness = None;
- }
-
- let _resigned = wallet.sign(&mut psbt, SignOptions::default())?;
-
- 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)}),
- )?)
- }
- }
- 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[..])
- .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?;
- }
-
- 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)
- .map_err(|e| Error::Generic(e.to_string()))?;
- tx_builder.add_data(
- &PushBytesBuf::try_from(op_return_data)
- .map_err(|e| Error::Generic(e.to_string()))?,
- );
- } else if let Some(string_data) = add_string {
- let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec())
- .map_err(|e| Error::Generic(e.to_string()))?;
- 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::<BTreeMap<String, Vec<usize>>>(&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::<Result<Vec<_>, 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<Psbt, Error>>(
- 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()) }),
- )?)
- }
- #[cfg(feature = "bip322")]
- SignMessage {
- message,
- signature_type,
- address,
- utxos,
- } => {
- let address: Address = parse_address(&address)?;
- let signature_format = parse_signature_format(&signature_type)?;
-
- if !wallet.is_mine(address.script_pubkey()) {
- return Err(Error::Generic(format!(
- "Address {} does not belong to this wallet.",
- address
- )));
- }
-
- let proof: MessageProof =
- wallet.sign_message(message.as_str(), signature_format, &address, utxos)?;
-
- Ok(json!({"proof": proof.to_base64()}).to_string())
- }
- #[cfg(feature = "bip322")]
- VerifyMessage {
- proof,
- message,
- address,
- } => {
- let address: Address = parse_address(&address)?;
- let parsed_proof: MessageProof = MessageProof::from_base64(&proof)
- .map_err(|e| BDKCliError::Generic(format!("Invalid proof: {e}")))?;
-
- let is_valid: MessageVerificationResult =
- wallet.verify_message(&parsed_proof, &message, &address)?;
-
- Ok(json!({
- "valid": is_valid.valid,
- "proven_amount": is_valid.proven_amount.map(|a| a.to_sat()) // optional field
- })
- .to_string())
- }
- }
-}
-
-/// 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: OnlineWalletSubCommand,
-) -> Result<String, Error> {
- 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::<KeychainKind>::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::<u8>::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;
- }
- }
-}
-
-/// Handle wallet config subcommand to create or update config.toml
-pub fn handle_config_subcommand(
- datadir: &Path,
- network: Network,
- wallet: String,
- wallet_opts: &WalletOpts,
- force: bool,
-) -> Result<String, Error> {
- if network == Network::Bitcoin {
- eprintln!(
- "WARNING: You are configuring a wallet for Bitcoin MAINNET.
- This software is experimental and not recommended for use with real funds.
- Consider using a testnet for testing purposes. \n"
- );
- }
-
- let ext_descriptor = wallet_opts.ext_descriptor.clone();
- let int_descriptor = wallet_opts.int_descriptor.clone();
-
- if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") {
- eprintln!(
- "WARNING: Your external descriptor contains PRIVATE KEYS.
- Private keys will be saved in PLAINTEXT in the config file.
- This is a security risk. Consider using public descriptors instead.\n"
- );
- }
-
- if let Some(ref internal_desc) = int_descriptor
- && (internal_desc.contains("xprv") || internal_desc.contains("tprv"))
- {
- eprintln!(
- "WARNING: Your internal descriptor contains PRIVATE KEYS.
- Private keys will be saved in PLAINTEXT in the config file.
- This is a security risk. Consider using public descriptors instead.\n"
- );
- }
-
- let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig {
- wallets: HashMap::new(),
- });
-
- if config.wallets.contains_key(&wallet) && !force {
- return Err(Error::Generic(format!(
- "Wallet '{wallet}' already exists in config.toml. Use --force to overwrite."
- )));
- }
-
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- let client_type = wallet_opts.client_type.clone();
- #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
- let url = &wallet_opts.url.clone();
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- let database_type = match wallet_opts.database_type {
- #[cfg(feature = "sqlite")]
- DatabaseType::Sqlite => "sqlite".to_string(),
- #[cfg(feature = "redb")]
- DatabaseType::Redb => "redb".to_string(),
- };
-
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- let client_type = match client_type {
- #[cfg(feature = "electrum")]
- ClientType::Electrum => "electrum".to_string(),
- #[cfg(feature = "esplora")]
- ClientType::Esplora => "esplora".to_string(),
- #[cfg(feature = "rpc")]
- ClientType::Rpc => "rpc".to_string(),
- #[cfg(feature = "cbf")]
- ClientType::Cbf => "cbf".to_string(),
- };
-
- let wallet_config = WalletConfigInner {
- wallet: wallet.clone(),
- network: network.to_string(),
- ext_descriptor,
- int_descriptor,
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- database_type,
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- client_type: Some(client_type),
- #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc",))]
- server_url: Some(url.to_string()),
- #[cfg(feature = "rpc")]
- rpc_user: Some(wallet_opts.basic_auth.0.clone()),
- #[cfg(feature = "rpc")]
- rpc_password: Some(wallet_opts.basic_auth.1.clone()),
- #[cfg(feature = "electrum")]
- batch_size: Some(wallet_opts.batch_size),
- #[cfg(feature = "esplora")]
- parallel_requests: Some(wallet_opts.parallel_requests),
- #[cfg(feature = "rpc")]
- cookie: wallet_opts.cookie.clone(),
- };
-
- config.wallets.insert(wallet.clone(), wallet_config);
- config.save(datadir)?;
-
- Ok(serde_json::to_string_pretty(&json!({
- "message": format!("Wallet '{wallet}' initialized successfully in {:?}", datadir.join("config.toml"))
- }))?)
-}
-
-/// Determine if PSBT has final script sigs or witnesses for all unsigned tx inputs.
-#[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
-))]
-pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> {
- let unsigned_tx_inputs = psbt.unsigned_tx.input.len();
- let psbt_inputs = psbt.inputs.len();
- if unsigned_tx_inputs != psbt_inputs {
- return Err(Error::Generic(format!(
- "Malformed PSBT, {unsigned_tx_inputs} unsigned tx inputs and {psbt_inputs} psbt inputs."
- )));
- }
- let sig_count = psbt.inputs.iter().fold(0, |count, input| {
- if input.final_script_sig.is_some() || input.final_script_witness.is_some() {
- count + 1
- } else {
- count
- }
- });
- if unsigned_tx_inputs > sig_count {
- return Err(Error::Generic(
- "The PSBT is not finalized, inputs are are not fully signed.".to_string(),
- ));
- }
- Ok(())
-}
-
-#[cfg(feature = "silent-payments")]
-pub(crate) fn handle_sp_subcommand(
- scan_pubkey: bdk_sp::bitcoin::secp256k1::PublicKey,
- spend_pubkey: bdk_sp::bitcoin::secp256k1::PublicKey,
- network: Network,
- pretty: bool,
-) -> Result<String, Error> {
- let sp_code = SilentPaymentCode::new_v0(scan_pubkey, spend_pubkey, network);
- if pretty {
- let table = vec![vec![
- "sp_code".cell().bold(true),
- sp_code.to_string().cell(),
- ]]
- .table()
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
- Ok(format!("{table}"))
- } else {
- Ok(serde_json::to_string_pretty(
- &json!({"sp_code": sp_code.to_string()}),
- )?)
- }
-}
-
-/// Handle a key sub-command
-///
-/// Key sub-commands are described in [`KeySubCommand`].
-pub(crate) fn handle_key_subcommand(
- network: Network,
- subcommand: KeySubCommand,
- pretty: bool,
-) -> Result<String, Error> {
- let secp = Secp256k1::new();
-
- match subcommand {
- KeySubCommand::Generate {
- word_count,
- password,
- } => {
- let mnemonic_type = match word_count {
- 12 => WordCount::Words12,
- _ => WordCount::Words24,
- };
- let mnemonic: GeneratedKey<_, miniscript::BareCtx> =
- Mnemonic::generate((mnemonic_type, Language::English))
- .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?;
- let mnemonic = mnemonic.into_key();
- let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?;
- let xprv = xkey.into_xprv(network).ok_or_else(|| {
- Error::Generic("Privatekey info not found (should not happen)".to_string())
- })?;
- let fingerprint = xprv.fingerprint(&secp);
- let phrase = mnemonic
- .words()
- .fold("".to_string(), |phrase, w| phrase + w + " ")
- .trim()
- .to_string();
- if pretty {
- let table = vec![
- vec![
- "Fingerprint".cell().bold(true),
- fingerprint.to_string().cell(),
- ],
- vec!["Mnemonic".cell().bold(true), mnemonic.to_string().cell()],
- vec!["Xprv".cell().bold(true), xprv.to_string().cell()],
- ]
- .table()
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
- Ok(format!("{table}"))
- } else {
- Ok(serde_json::to_string_pretty(
- &json!({ "mnemonic": phrase, "xprv": xprv.to_string(), "fingerprint": fingerprint.to_string() }),
- )?)
- }
- }
- KeySubCommand::Restore { mnemonic, password } => {
- let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)?;
- let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?;
- let xprv = xkey.into_xprv(network).ok_or_else(|| {
- Error::Generic("Privatekey info not found (should not happen)".to_string())
- })?;
- let fingerprint = xprv.fingerprint(&secp);
- if pretty {
- let table = vec![
- vec![
- "Fingerprint".cell().bold(true),
- fingerprint.to_string().cell(),
- ],
- vec!["Mnemonic".cell().bold(true), mnemonic.to_string().cell()],
- vec!["Xprv".cell().bold(true), xprv.to_string().cell()],
- ]
- .table()
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
- Ok(format!("{table}"))
- } else {
- Ok(serde_json::to_string_pretty(
- &json!({ "xprv": xprv.to_string(), "fingerprint": fingerprint.to_string() }),
- )?)
- }
- }
- KeySubCommand::Derive { xprv, path } => {
- if xprv.network != network.into() {
- return Err(Error::Generic("Invalid network".to_string()));
- }
- let derived_xprv = &xprv.derive_priv(&secp, &path)?;
-
- let origin: KeySource = (xprv.fingerprint(&secp), path);
-
- let derived_xprv_desc_key: DescriptorKey<Segwitv0> =
- derived_xprv.into_descriptor_key(Some(origin), DerivationPath::default())?;
-
- if let Secret(desc_seckey, _, _) = derived_xprv_desc_key {
- let desc_pubkey = desc_seckey.to_public(&secp)?;
- if pretty {
- let table = vec![
- vec!["Xpub".cell().bold(true), desc_pubkey.to_string().cell()],
- vec!["Xprv".cell().bold(true), xprv.to_string().cell()],
- ]
- .table()
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
- Ok(format!("{table}"))
- } else {
- Ok(serde_json::to_string_pretty(
- &json!({"xpub": desc_pubkey.to_string(), "xprv": desc_seckey.to_string()}),
- )?)
- }
- } else {
- Err(Error::Generic("Invalid key variant".to_string()))
- }
- }
- }
-}
-
-/// Handle the miniscript compiler sub-command
-///
-/// Compiler options are described in [`CliSubCommand::Compile`].
-#[cfg(feature = "compiler")]
-pub(crate) fn handle_compile_subcommand(
- _network: Network,
- policy: String,
- script_type: String,
- pretty: bool,
-) -> Result<String, Error> {
- let policy = Concrete::<String>::from_str(policy.as_str())?;
- let legacy_policy: Miniscript<String, Legacy> = policy
- .compile()
- .map_err(|e| Error::Generic(e.to_string()))?;
- let segwit_policy: Miniscript<String, Segwitv0> = policy
- .compile()
- .map_err(|e| Error::Generic(e.to_string()))?;
- let taproot_policy: Miniscript<String, Tap> = policy
- .compile()
- .map_err(|e| Error::Generic(e.to_string()))?;
-
- let descriptor = match script_type.as_str() {
- "sh" => Descriptor::new_sh(legacy_policy),
- "wsh" => Descriptor::new_wsh(segwit_policy),
- "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
- "tr" => {
- // For tr descriptors, we use a well-known unspendable key (NUMS point).
- // This ensures the key path is effectively disabled and only script path can be used.
- // See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
-
- let xonly_public_key = XOnlyPublicKey::from_str(NUMS_UNSPENDABLE_KEY_HEX)
- .map_err(|e| Error::Generic(format!("Invalid NUMS key: {e}")))?;
-
- let tree = TapTree::Leaf(Arc::new(taproot_policy));
- Descriptor::new_tr(xonly_public_key.to_string(), Some(tree))
- }
- _ => {
- return Err(Error::Generic(
- "Invalid script type. Supported types: sh, wsh, sh-wsh, tr".to_string(),
- ));
- }
- }?;
- if pretty {
- let table = vec![vec![
- "Descriptor".cell().bold(true),
- descriptor.to_string().cell(),
- ]]
- .table()
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
- Ok(format!("{table}"))
- } else {
- Ok(serde_json::to_string_pretty(
- &json!({"descriptor": descriptor.to_string()}),
- )?)
- }
-}
-
-/// Handle wallets command to show all saved wallet configurations
-pub fn handle_wallets_subcommand(datadir: &Path, pretty: bool) -> Result<String, Error> {
- let load_config = WalletConfig::load(datadir)?;
-
- let config = match load_config {
- Some(c) if !c.wallets.is_empty() => c,
- _ => {
- return Ok(if pretty {
- "No wallet configurations found.".to_string()
- } else {
- serde_json::to_string_pretty(&json!({
- "wallets": []
- }))?
- });
- }
- };
-
- if pretty {
- let mut rows: Vec<Vec<CellStruct>> = vec![];
-
- for (name, wallet_config) in config.wallets.iter() {
- let mut row = vec![name.cell(), wallet_config.network.clone().cell()];
-
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- row.push(wallet_config.database_type.clone().cell());
-
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- {
- let client_str = wallet_config.client_type.as_deref().unwrap_or("N/A");
- row.push(client_str.cell());
- }
-
- #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
- {
- let url_str = wallet_config.server_url.as_deref().unwrap_or("N/A");
- let display_url = if url_str.len() > 20 {
- shorten(url_str, 15, 10)
- } else {
- url_str.to_string()
- };
- row.push(display_url.cell());
- }
-
- let ext_desc_display = if wallet_config.ext_descriptor.len() > 40 {
- shorten(&wallet_config.ext_descriptor, 20, 15)
- } else {
- wallet_config.ext_descriptor.clone()
- };
- row.push(ext_desc_display.cell());
-
- let has_int_desc = if wallet_config.int_descriptor.is_some() {
- "Yes"
- } else {
- "No"
- };
- row.push(has_int_desc.cell());
-
- rows.push(row);
- }
-
- let mut title_cells = vec!["Wallet Name".cell().bold(true), "Network".cell().bold(true)];
-
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- title_cells.push("Database".cell().bold(true));
-
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- title_cells.push("Client".cell().bold(true));
-
- #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
- title_cells.push("Server URL".cell().bold(true));
-
- title_cells.push("External Desc".cell().bold(true));
- title_cells.push("Internal Desc".cell().bold(true));
-
- let table = rows
- .table()
- .title(title_cells)
- .display()
- .map_err(|e| Error::Generic(e.to_string()))?;
-
- Ok(format!("{table}"))
- } else {
- let wallets_summary: Vec<_> = config
- .wallets
- .iter()
- .map(|(name, wallet_config)| {
- #[allow(unused_mut)]
- let mut wallet_json = json!({
- "name": name,
- "network": wallet_config.network,
- "ext_descriptor": wallet_config.ext_descriptor,
- "int_descriptor": wallet_config.int_descriptor,
- });
-
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- {
- wallet_json["database_type"] = json!(wallet_config.database_type.clone());
- }
-
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- {
- wallet_json["client_type"] = json!(wallet_config.client_type.clone());
- }
-
- #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
- {
- wallet_json["server_url"] = json!(wallet_config.server_url.clone());
- }
-
- wallet_json
- })
- .collect();
-
- Ok(serde_json::to_string_pretty(&json!({
- "wallets": wallets_summary
- }))?)
- }
-}
-
-/// The global top level handler.
-pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
- let pretty = cli_opts.pretty;
- let subcommand = cli_opts.subcommand.clone();
-
- let result: Result<String, Error> = match subcommand {
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
- ))]
- CliSubCommand::Wallet {
- wallet,
- subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
- } => {
- let home_dir = prepare_home_dir(cli_opts.datadir)?;
-
- let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet)?;
-
- let database_path = prepare_wallet_db_dir(&home_dir, &wallet)?;
-
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- let result = {
- let mut persister: Persister = match &wallet_opts.database_type {
- #[cfg(feature = "sqlite")]
- DatabaseType::Sqlite => {
- let db_file = database_path.join("wallet.sqlite");
- let connection = Connection::open(db_file)?;
- log::debug!("Sqlite database opened successfully");
- Persister::Connection(connection)
- }
- #[cfg(feature = "redb")]
- DatabaseType::Redb => {
- let wallet_name = &wallet_opts.wallet;
- let db = Arc::new(bdk_redb::redb::Database::create(
- home_dir.join("wallet.redb"),
- )?);
- let store = RedbStore::new(
- db,
- wallet_name.as_deref().unwrap_or("wallet").to_string(),
- )?;
- log::debug!("Redb database opened successfully");
- Persister::RedbStore(store)
- }
- };
-
- let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
- let blockchain_client =
- new_blockchain_client(&wallet_opts, &wallet, database_path)?;
-
- let result = handle_online_wallet_subcommand(
- &mut wallet,
- &blockchain_client,
- online_subcommand,
- )
- .await?;
- wallet.persist(&mut persister)?;
- result
- };
- #[cfg(not(any(feature = "sqlite", feature = "redb")))]
- let result = {
- let mut wallet = new_wallet(network, wallet_opts)?;
- let blockchain_client =
- crate::utils::new_blockchain_client(wallet_opts, &wallet, database_path)?;
- handle_online_wallet_subcommand(&mut wallet, &blockchain_client, online_subcommand)
- .await?
- };
- Ok(result)
- }
- CliSubCommand::Wallet {
- wallet: wallet_name,
- subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
- } => {
- let datadir = cli_opts.datadir.clone();
- let home_dir = prepare_home_dir(datadir)?;
- let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
-
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- let result = {
- let mut persister: Persister = match &wallet_opts.database_type {
- #[cfg(feature = "sqlite")]
- DatabaseType::Sqlite => {
- let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
- let db_file = database_path.join("wallet.sqlite");
- let connection = Connection::open(db_file)?;
- log::debug!("Sqlite database opened successfully");
- Persister::Connection(connection)
- }
- #[cfg(feature = "redb")]
- DatabaseType::Redb => {
- let db = Arc::new(bdk_redb::redb::Database::create(
- home_dir.join("wallet.redb"),
- )?);
- let store = RedbStore::new(db, wallet_name)?;
- log::debug!("Redb database opened successfully");
- Persister::RedbStore(store)
- }
- };
-
- let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
-
- let result = handle_offline_wallet_subcommand(
- &mut wallet,
- &wallet_opts,
- &cli_opts,
- offline_subcommand.clone(),
- )?;
- wallet.persist(&mut persister)?;
- result
- };
- #[cfg(not(any(feature = "sqlite", feature = "redb")))]
- let result = {
- let mut wallet = new_wallet(network, &wallet_opts)?;
- handle_offline_wallet_subcommand(
- &mut wallet,
- &wallet_opts,
- &cli_opts,
- offline_subcommand.clone(),
- )?
- };
- Ok(result)
- }
- CliSubCommand::Wallet {
- wallet,
- subcommand: WalletSubCommand::Config { force, wallet_opts },
- } => {
- let network = cli_opts.network;
- let home_dir = prepare_home_dir(cli_opts.datadir)?;
- let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?;
- Ok(result)
- }
- CliSubCommand::Wallets => {
- let home_dir = prepare_home_dir(cli_opts.datadir)?;
- let result = handle_wallets_subcommand(&home_dir, pretty)?;
- Ok(result)
- }
- CliSubCommand::Key {
- subcommand: key_subcommand,
- } => {
- let network = cli_opts.network;
- let result = handle_key_subcommand(network, key_subcommand, pretty)?;
- Ok(result)
- }
- #[cfg(feature = "silent-payments")]
- CliSubCommand::SilentPaymentCode { scan, spend } => {
- let network = cli_opts.network;
- let result = handle_sp_subcommand(scan, spend, network, pretty)?;
- Ok(result)
- }
- #[cfg(feature = "compiler")]
- CliSubCommand::Compile {
- policy,
- script_type,
- } => {
- let network = cli_opts.network;
- let result = handle_compile_subcommand(network, policy, script_type, pretty)?;
- Ok(result)
- }
- #[cfg(feature = "repl")]
- CliSubCommand::Repl {
- wallet: wallet_name,
- } => {
- let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
- let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
-
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- let (mut wallet, mut persister) = {
- let mut persister: Persister = match &wallet_opts.database_type {
- #[cfg(feature = "sqlite")]
- DatabaseType::Sqlite => {
- let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
- let db_file = database_path.join("wallet.sqlite");
- let connection = Connection::open(db_file)?;
- log::debug!("Sqlite database opened successfully");
- Persister::Connection(connection)
- }
- #[cfg(feature = "redb")]
- DatabaseType::Redb => {
- let db = Arc::new(bdk_redb::redb::Database::create(
- home_dir.join("wallet.redb"),
- )?);
- let store = RedbStore::new(db, wallet_name.clone())?;
- log::debug!("Redb database opened successfully");
- Persister::RedbStore(store)
- }
- };
- let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
- (wallet, persister)
- };
- #[cfg(not(any(feature = "sqlite", feature = "redb")))]
- let mut wallet = new_wallet(network, &loaded_wallet_opts)?;
- let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
- let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
- loop {
- let line = readline()?;
- let line = line.trim();
- if line.is_empty() {
- continue;
- }
-
- let result = respond(
- network,
- &mut wallet,
- &wallet_name,
- &mut wallet_opts.clone(),
- line,
- database_path.clone(),
- &cli_opts,
- )
- .await;
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- wallet.persist(&mut persister)?;
-
- match result {
- Ok(quit) => {
- if quit {
- break;
- }
- }
- Err(err) => {
- writeln!(std::io::stdout(), "{err}")
- .map_err(|e| Error::Generic(e.to_string()))?;
- std::io::stdout()
- .flush()
- .map_err(|e| Error::Generic(e.to_string()))?;
- }
- }
- }
- Ok("".to_string())
- }
- CliSubCommand::Descriptor { desc_type, key } => {
- let descriptor = handle_descriptor_command(cli_opts.network, desc_type, key, pretty)?;
- Ok(descriptor)
- }
- CliSubCommand::Completions { shell } => {
- clap_complete::generate(
- shell,
- &mut CliOpts::command(),
- "bdk-cli",
- &mut std::io::stdout(),
- );
-
- Ok("".to_string())
- }
- };
- result
-}
-
-#[cfg(feature = "repl")]
-async fn respond(
- network: Network,
- wallet: &mut Wallet,
- wallet_name: &String,
- wallet_opts: &mut WalletOpts,
- line: &str,
- _datadir: std::path::PathBuf,
- cli_opts: &CliOpts,
-) -> Result<bool, String> {
- use clap::Parser;
-
- let args = shlex::split(line).ok_or("error: Invalid quoting".to_string())?;
- let repl_subcommand = ReplSubCommand::try_parse_from(args).map_err(|e| e.to_string())?;
- let response = match repl_subcommand {
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
- ))]
- ReplSubCommand::Wallet {
- subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
- } => {
- let blockchain =
- new_blockchain_client(wallet_opts, wallet, _datadir).map_err(|e| e.to_string())?;
- let value = handle_online_wallet_subcommand(wallet, &blockchain, online_subcommand)
- .await
- .map_err(|e| e.to_string())?;
- Some(value)
- }
- ReplSubCommand::Wallet {
- subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
- } => {
- let value =
- handle_offline_wallet_subcommand(wallet, wallet_opts, cli_opts, offline_subcommand)
- .map_err(|e| e.to_string())?;
- Some(value)
- }
- ReplSubCommand::Wallet {
- subcommand: WalletSubCommand::Config { force, wallet_opts },
- } => {
- let value = handle_config_subcommand(
- &_datadir,
- network,
- wallet_name.to_string(),
- &wallet_opts,
- force,
- )
- .map_err(|e| e.to_string())?;
- Some(value)
- }
- ReplSubCommand::Key { subcommand } => {
- let value = handle_key_subcommand(network, subcommand, cli_opts.pretty)
- .map_err(|e| e.to_string())?;
- Some(value)
- }
- ReplSubCommand::Descriptor { desc_type, key } => {
- let value = handle_descriptor_command(network, desc_type, key, cli_opts.pretty)
- .map_err(|e| e.to_string())?;
- Some(value)
- }
- ReplSubCommand::Exit => None,
- };
- if let Some(value) = response {
- writeln!(std::io::stdout(), "{value}").map_err(|e| e.to_string())?;
- std::io::stdout().flush().map_err(|e| e.to_string())?;
- Ok(false)
- } else {
- writeln!(std::io::stdout(), "Exiting...").map_err(|e| e.to_string())?;
- std::io::stdout().flush().map_err(|e| e.to_string())?;
- Ok(true)
- }
-}
-
-#[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<Txid, Error> {
- 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)
- }
- }
-}
-
-#[cfg(feature = "repl")]
-fn readline() -> Result<String, Error> {
- write!(std::io::stdout(), "> ").map_err(|e| Error::Generic(e.to_string()))?;
- std::io::stdout()
- .flush()
- .map_err(|e| Error::Generic(e.to_string()))?;
- let mut buffer = String::new();
- std::io::stdin()
- .read_line(&mut buffer)
- .map_err(|e| Error::Generic(e.to_string()))?;
- Ok(buffer)
-}
-
-/// Handle the descriptor command
-pub fn handle_descriptor_command(
- network: Network,
- desc_type: String,
- key: Option<String>,
- pretty: bool,
-) -> Result<String, Error> {
- let result = match key {
- Some(key) => {
- if is_mnemonic(&key) {
- // User provided mnemonic
- generate_descriptor_from_mnemonic(&key, network, &desc_type)
- } else {
- // User provided xprv/xpub
- generate_descriptors(&desc_type, &key, network)
- }
- }
- // Generate new mnemonic and descriptors
- None => generate_descriptor_with_mnemonic(network, &desc_type),
- }?;
- format_descriptor_output(&result, pretty)
-}
-
-#[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
-))]
-#[cfg(test)]
-mod test {
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "cbf",
- feature = "rpc"
- ))]
- #[test]
- fn test_psbt_is_final() {
- use super::is_final;
- use bdk_wallet::bitcoin::Psbt;
- use std::str::FromStr;
-
- let unsigned_psbt = Psbt::from_str("cHNidP8BAIkBAAAAASWJHzxzyVORV/C3lAynKHVVL7+Rw7/Jj8U9fuvD24olAAAAAAD+////AiBOAAAAAAAAIgAgLzY9yE4jzTFJnHtTjkc+rFAtJ9NB7ENFQ1xLYoKsI1cfqgKVAAAAACIAIFsbWgDeLGU8EA+RGwBDIbcv4gaGG0tbEIhDvwXXa/E7LwEAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BALLAAD/////AgD5ApUAAAAAIgAgWxtaAN4sZTwQD5EbAEMhty/iBoYbS1sQiEO/Bddr8TsAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErAPkClQAAAAAiACBbG1oA3ixlPBAPkRsAQyG3L+IGhhtLWxCIQ78F12vxOwEFR1IhA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDIQLKhV/gEZYmlsQXnsL5/Uqv5Y8O31tmWW1LQqIBkiqzCVKuIgYCyoVf4BGWJpbEF57C+f1Kr+WPDt9bZlltS0KiAZIqswkEboH3lCIGA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDBDS6ZSEAACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==").unwrap();
- assert!(is_final(&unsigned_psbt).is_err());
-
- let part_signed_psbt = Psbt::from_str("cHNidP8BAIkBAAAAASWJHzxzyVORV/C3lAynKHVVL7+Rw7/Jj8U9fuvD24olAAAAAAD+////AiBOAAAAAAAAIgAgLzY9yE4jzTFJnHtTjkc+rFAtJ9NB7ENFQ1xLYoKsI1cfqgKVAAAAACIAIFsbWgDeLGU8EA+RGwBDIbcv4gaGG0tbEIhDvwXXa/E7LwEAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BALLAAD/////AgD5ApUAAAAAIgAgWxtaAN4sZTwQD5EbAEMhty/iBoYbS1sQiEO/Bddr8TsAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErAPkClQAAAAAiACBbG1oA3ixlPBAPkRsAQyG3L+IGhhtLWxCIQ78F12vxOyICA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDSDBFAiEAnNPpu6wNX2HXYz8s2q5nXug4cWfvCGD3SSH2CNKm+yECIEQO7/URhUPsGoknMTE+GrYJf9Wxqn9QsuN9FGj32cQpAQEFR1IhA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDIQLKhV/gEZYmlsQXnsL5/Uqv5Y8O31tmWW1LQqIBkiqzCVKuIgYCyoVf4BGWJpbEF57C+f1Kr+WPDt9bZlltS0KiAZIqswkEboH3lCIGA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDBDS6ZSEAACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==").unwrap();
- assert!(is_final(&part_signed_psbt).is_err());
-
- let full_signed_psbt = Psbt::from_str("cHNidP8BAIkBAAAAASWJHzxzyVORV/C3lAynKHVVL7+Rw7/Jj8U9fuvD24olAAAAAAD+////AiBOAAAAAAAAIgAgLzY9yE4jzTFJnHtTjkc+rFAtJ9NB7ENFQ1xLYoKsI1cfqgKVAAAAACIAIFsbWgDeLGU8EA+RGwBDIbcv4gaGG0tbEIhDvwXXa/E7LwEAAAABALUCAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BALLAAD/////AgD5ApUAAAAAIgAgWxtaAN4sZTwQD5EbAEMhty/iBoYbS1sQiEO/Bddr8TsAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQErAPkClQAAAAAiACBbG1oA3ixlPBAPkRsAQyG3L+IGhhtLWxCIQ78F12vxOwEFR1IhA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDIQLKhV/gEZYmlsQXnsL5/Uqv5Y8O31tmWW1LQqIBkiqzCVKuIgYCyoVf4BGWJpbEF57C+f1Kr+WPDt9bZlltS0KiAZIqswkEboH3lCIGA/JV2U/0pXW+iP49QcsYilEvkZEd4phmDM8nV8wC+MeDBDS6ZSEBBwABCNsEAEgwRQIhAJzT6busDV9h12M/LNquZ17oOHFn7whg90kh9gjSpvshAiBEDu/1EYVD7BqJJzExPhq2CX/Vsap/ULLjfRRo99nEKQFHMEQCIGoFCvJ2zPB7PCpznh4+1jsY03kMie49KPoPDdr7/T9TAiB3jV7wzR9BH11FSbi+8U8gSX95PrBlnp1lOBgTUIUw3QFHUiED8lXZT/Sldb6I/j1ByxiKUS+RkR3imGYMzydXzAL4x4MhAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJUq4AACICAsqFX+ARliaWxBeewvn9Sq/ljw7fW2ZZbUtCogGSKrMJBG6B95QiAgPyVdlP9KV1voj+PUHLGIpRL5GRHeKYZgzPJ1fMAvjHgwQ0umUhAA==").unwrap();
- assert!(is_final(&full_signed_psbt).is_ok());
- }
-
- #[cfg(feature = "compiler")]
- #[test]
- fn test_compile_taproot() {
- use super::{NUMS_UNSPENDABLE_KEY_HEX, handle_compile_subcommand};
- use bdk_wallet::bitcoin::Network;
-
- // Expected taproot descriptors with checksums (using NUMS key from constant)
- let expected_pk_a = format!("tr({},pk(A))#a2mlskt0", NUMS_UNSPENDABLE_KEY_HEX);
- let expected_and_ab = format!(
- "tr({},and_v(v:pk(A),pk(B)))#sfplm6kv",
- NUMS_UNSPENDABLE_KEY_HEX
- );
-
- // Test simple pk policy compilation to taproot
- let result = handle_compile_subcommand(
- Network::Testnet,
- "pk(A)".to_string(),
- "tr".to_string(),
- false,
- );
- assert!(result.is_ok());
- let json_string = result.unwrap();
- let json_result: serde_json::Value = serde_json::from_str(&json_string).unwrap();
- let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
- assert_eq!(descriptor, expected_pk_a);
-
- // Test more complex policy
- let result = handle_compile_subcommand(
- Network::Testnet,
- "and(pk(A),pk(B))".to_string(),
- "tr".to_string(),
- false,
- );
- assert!(result.is_ok());
- let json_string = result.unwrap();
- let json_result: serde_json::Value = serde_json::from_str(&json_string).unwrap();
- let descriptor = json_result.get("descriptor").unwrap().as_str().unwrap();
- assert_eq!(descriptor, expected_and_ab);
- }
-
- #[cfg(feature = "compiler")]
- #[test]
- fn test_compile_invalid_cases() {
- use super::handle_compile_subcommand;
- use bdk_wallet::bitcoin::Network;
-
- // Test invalid policy syntax
- let result = handle_compile_subcommand(
- Network::Testnet,
- "invalid_policy".to_string(),
- "tr".to_string(),
- false,
- );
- assert!(result.is_err());
-
- // Test invalid script type
- let result = handle_compile_subcommand(
- Network::Testnet,
- "pk(A)".to_string(),
- "invalid_type".to_string(),
- false,
- );
- assert!(result.is_err());
-
- // Test empty policy
- let result =
- handle_compile_subcommand(Network::Testnet, "".to_string(), "tr".to_string(), false);
- assert!(result.is_err());
-
- // Test malformed policy with unmatched parentheses
- let result = handle_compile_subcommand(
- Network::Testnet,
- "pk(A".to_string(),
- "tr".to_string(),
- false,
- );
- assert!(result.is_err());
-
- // Test policy with unknown function
- let result = handle_compile_subcommand(
- Network::Testnet,
- "unknown_func(A)".to_string(),
- "tr".to_string(),
- false,
- );
- assert!(result.is_err());
- }
-}