// Copyright (c) 2020-2021 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.
//! bdk-cli Command structure
//!
//! This module defines all the bdk-cli commands structure.
//! All optional args are defined in the structs below.
//! All subcommands are defined in the below enums.
#![allow(clippy::large_enum_variant)]
use clap::{AppSettings, Parser, Subcommand};
use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
use bdk::bitcoin::{Address, Network, OutPoint, Script};
use crate::utils::{parse_outpoint, parse_recipient};
#[cfg(any(
feature = "compact_filters",
feature = "electrum",
feature = "esplora",
feature = "rpc"
))]
use {crate::utils::parse_proxy_auth, clap::Args};
#[derive(PartialEq, Clone, Debug, Parser)]
/// The BDK Command Line Wallet App
///
/// bdk-cli is a light weight command line bitcoin wallet, powered by BDK.
/// This app can be used as a playground as well as testing environment to simulate
/// various wallet testing situations. If you are planning to use BDK in your wallet, bdk-cli
/// is also a great intro tool to get familiar with the BDK API.
///
/// But this is not just any toy.
/// bdk-cli is also a fully functioning Bitcoin wallet with taproot support!
///
/// For more information checkout <https://bitcoindevkit.org/>
#[clap(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
pub struct CliOpts {
/// Sets the network.
#[clap(
name = "NETWORK",
short = 'n',
long = "network",
default_value = "testnet",
possible_values = &["bitcoin", "testnet", "signet", "regtest"]
)]
pub network: Network,
/// Sets the wallet data directory.
/// Default value : "~/.bdk-bitcoin
#[clap(name = "DATADIR", short = 'd', long = "datadir")]
pub datadir: Option<std::path::PathBuf>,
/// Top level cli sub-commands.
#[clap(subcommand)]
pub subcommand: CliSubCommand,
}
/// Top level cli sub-commands.
#[derive(Debug, Subcommand, Clone, PartialEq)]
#[clap(rename_all = "snake")]
pub enum CliSubCommand {
/// Node operation subcommands.
///
/// These commands can be used to control the backend bitcoin-core node
/// launched automatically with the `regtest-*` feature sets. The commands issue
/// bitcoin-cli rpc calls on the daemon, in the background.
///
/// Feel free to open a feature request issue in <https://github.com/bitcoindevkit/bdk-cli>
/// if you need extra rpc calls not covered in the command list.
#[cfg(feature = "regtest-node")]
#[clap(long_about = "Regtest Node mode")]
Node {
#[clap(subcommand)]
subcommand: NodeSubCommand,
},
/// Wallet operations.
///
/// bdk-cli wallet operations includes all the basic wallet level tasks.
/// Most commands can be used without connecting to any backend. To use commands that
/// needs backend like `sync` and `broadcast`, compile the binary with specific backend feature
/// and use the configuration options below to configure for that backend.
Wallet {
#[clap(flatten)]
wallet_opts: WalletOpts,
#[clap(subcommand)]
subcommand: WalletSubCommand,
},
/// Key management operations.
///
/// Provides basic key operations that are not related to a specific wallet such as generating a
/// new random master extended key or restoring a master extended key from mnemonic words.
///
/// These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this
/// feature to create keys that secure actual funds on the Bitcoin mainnet.
Key {
#[clap(subcommand)]
subcommand: KeySubCommand,
},
/// Compile a miniscript policy to an output descriptor.
#[cfg(feature = "compiler")]
#[clap(long_about = "Miniscript policy compiler")]
Compile {
/// Sets the spending policy to compile.
#[clap(name = "POLICY", required = true, index = 1)]
policy: String,
/// Sets the script type used to embed the compiled policy.
#[clap(name = "TYPE", short = 't', long = "type", default_value = "wsh", possible_values = &["sh","wsh", "sh-wsh"])]
script_type: String,
},
#[cfg(feature = "repl")]
/// REPL command loop mode.
///
/// REPL command loop can be used to make recurring callbacks to an already loaded wallet.
/// This mode is useful for hands on live testing of wallet operations.
Repl {
#[clap(flatten)]
wallet_opts: WalletOpts,
},
/// Proof of reserves operations.
///
/// This can be used to produce and verify Proof of Reserves (similar to BIP 322) using bdk-cli.
#[cfg(all(feature = "reserves", feature = "electrum"))]
ExternalReserves {
/// Sets the challenge message with which the proof was produced.
#[clap(name = "MESSAGE", required = true, index = 1)]
message: String,
/// Sets the proof in form of a PSBT to verify.
#[clap(name = "PSBT", required = true, index = 2)]
psbt: String,
/// Sets the number of block confirmations for UTXOs to be considered.
#[clap(name = "CONFIRMATIONS", required = true, index = 3)]
confirmations: usize,
/// Sets the addresses for which the proof was produced.
#[clap(name = "ADDRESSES", required = true, index = 4)]
addresses: Vec<String>,
#[clap(flatten)]
electrum_opts: ElectrumOpts,
},
}
/// Backend Node operation subcommands.
#[derive(Debug, Subcommand, Clone, PartialEq)]
#[clap(rename_all = "lower")]
#[cfg(any(feature = "regtest-node"))]
pub enum NodeSubCommand {
/// Get info.
GetInfo,
/// Get new address from node's test wallet.
GetNewAddress,
/// Generate given number of blocks and fund the internal wallet with coinbases.
Generate { block_num: u64 },
/// Get Wallet balance.
GetBalance,
/// Send to an external wallet address.
SendToAddress { address: String, amount: u64 },
/// Execute any bitcoin-cli commands.
#[clap(external_subcommand)]
BitcoinCli(Vec<String>),
}
/// Wallet operation subcommands.
#[derive(Debug, Subcommand, Clone, PartialEq)]
pub enum WalletSubCommand {
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "compact_filters",
feature = "rpc"
))]
#[clap(flatten)]
OnlineWalletSubCommand(OnlineWalletSubCommand),
#[clap(flatten)]
OfflineWalletSubCommand(OfflineWalletSubCommand),
}
/// Config options wallet operations can take.
#[derive(Debug, Parser, Clone, PartialEq)]
pub struct WalletOpts {
/// Selects the wallet to use.
#[clap(name = "WALLET_NAME", short = 'w', long = "wallet")]
pub wallet: Option<String>,
/// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects.
#[clap(name = "VERBOSE", short = 'v', long = "verbose")]
pub verbose: bool,
/// Sets the descriptor to use for the external addresses.
#[clap(name = "DESCRIPTOR", short = 'd', long = "descriptor", required = true)]
pub descriptor: String,
/// Sets the descriptor to use for internal addresses.
#[clap(name = "CHANGE_DESCRIPTOR", short = 'c', long = "change_descriptor")]
pub change_descriptor: Option<String>,
#[cfg(feature = "electrum")]
#[clap(flatten)]
pub electrum_opts: ElectrumOpts,
#[cfg(feature = "esplora")]
#[clap(flatten)]
pub esplora_opts: EsploraOpts,
#[cfg(feature = "compact_filters")]
#[clap(flatten)]
pub compactfilter_opts: CompactFilterOpts,
#[cfg(feature = "rpc")]
#[clap(flatten)]
pub rpc_opts: RpcOpts,
#[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
#[clap(flatten)]
pub proxy_opts: ProxyOpts,
}
/// Options to configure a SOCKS5 proxy for a blockchain client connection.
#[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
#[derive(Debug, Args, Clone, PartialEq)]
pub struct ProxyOpts {
/// Sets the SOCKS5 proxy for a blockchain client.
#[clap(name = "PROXY_ADDRS:PORT", long = "proxy", short = 'p')]
pub proxy: Option<String>,
/// Sets the SOCKS5 proxy credential.
#[clap(name="PROXY_USER:PASSWD", long="proxy_auth", short='a', value_parser = parse_proxy_auth)]
pub proxy_auth: Option<(String, String)>,
/// Sets the SOCKS5 proxy retries for the blockchain client.
#[clap(
name = "PROXY_RETRIES",
short = 'r',
long = "retries",
default_value = "5"
)]
pub retries: u8,
}
/// Options to configure a BIP157 Compact Filter backend.
#[cfg(feature = "compact_filters")]
#[derive(Debug, Args, Clone, PartialEq)]
pub struct CompactFilterOpts {
/// Sets the full node network address.
#[clap(
name = "ADDRESS:PORT",
short = 'n',
long = "node",
default_value = "127.0.0.1:18444"
)]
pub address: Vec<String>,
/// Sets the number of parallel node connections.
#[clap(name = "CONNECTIONS", long = "conn_count", default_value = "4")]
pub conn_count: usize,
/// Optionally skip initial `skip_blocks` blocks.
#[clap(
name = "SKIP_BLOCKS",
short = 'k',
long = "skip_blocks",
default_value = "0"
)]
pub skip_blocks: usize,
}
/// Options to configure a bitcoin core rpc backend.
#[cfg(feature = "rpc")]
#[derive(Debug, Args, Clone, PartialEq)]
pub struct RpcOpts {
/// Sets the full node address for rpc connection.
#[clap(
name = "ADDRESS:PORT",
short = 'n',
long = "node",
default_value = "127.0.0.1:18443"
)]
pub address: String,
/// Sets the rpc basic authentication.
#[clap(
name = "USER:PASSWD",
short = 'a',
long = "basic-auth",
value_parser = parse_proxy_auth,
default_value = "user:password",
)]
pub basic_auth: (String, String),
/// Sets an optional cookie authentication.
#[clap(name = "COOKIE", long = "cookie")]
pub cookie: Option<String>,
/// Time in unix seconds in which initial sync will start scanning from (0 to start from genesis).
#[clap(
name = "RPC_START_TIME",
short = 's',
long = "start-time",
default_value = "0"
)]
pub start_time: u64,
}
/// Options to configure electrum backend.
#[cfg(feature = "electrum")]
#[derive(Debug, Args, Clone, PartialEq)]
pub struct ElectrumOpts {
/// Sets the SOCKS5 proxy timeout for the Electrum client.
#[clap(name = "PROXY_TIMEOUT", short = 't', long = "timeout")]
pub timeout: Option<u8>,
/// Sets the Electrum server to use.
#[clap(
name = "ELECTRUM_URL",
short = 's',
long = "server",
default_value = "ssl://electrum.blockstream.info:60002"
)]
pub server: String,
/// Stop searching addresses for transactions after finding an unused gap of this length.
#[clap(
name = "STOP_GAP",
long = "stop_gap",
short = 'g',
default_value = "10"
)]
pub stop_gap: usize,
}
/// Options to configure Esplora backend.
#[cfg(feature = "esplora")]
#[derive(Debug, Args, Clone, PartialEq)]
pub struct EsploraOpts {
/// Use the esplora server if given as parameter.
#[clap(
name = "ESPLORA_URL",
short = 's',
long = "server",
default_value = "https://blockstream.info/testnet/api/"
)]
pub server: String,
/// Socket timeout.
#[clap(name = "TIMEOUT", long = "timeout", default_value = "5")]
pub timeout: u64,
/// Stop searching addresses for transactions after finding an unused gap of this length.
#[clap(
name = "STOP_GAP",
long = "stop_gap",
short = 'g',
default_value = "10"
)]
pub stop_gap: usize,
/// Number of parallel requests sent to the esplora service.
#[clap(name = "CONCURRENCY", long = "conc", default_value = "4")]
pub conc: u8,
}
/// Wallet subcommands that can be issued without a blockchain backend.
#[derive(Debug, Subcommand, Clone, PartialEq)]
#[clap(rename_all = "snake")]
pub enum OfflineWalletSubCommand {
/// Generates a new external address.
GetNewAddress,
/// Lists the available spendable UTXOs.
ListUnspent,
/// Lists all the incoming and outgoing transactions of the wallet.
ListTransactions,
/// Returns the current wallet balance.
GetBalance,
/// Creates a new unsigned transaction.
CreateTx {
/// Adds a recipient to the transaction.
// Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
// Address and amount parsing is done at run time in handler function.
#[clap(name = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]
recipients: Vec<(Script, u64)>,
/// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
#[clap(long = "send_all", short = 'a')]
send_all: bool,
/// Enables Replace-By-Fee (BIP125).
#[clap(long = "enable_rbf", short = 'r')]
enable_rbf: bool,
/// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
#[clap(long = "offline_signer")]
offline_signer: bool,
/// Selects which utxos *must* be spent.
#[clap(name = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
utxos: Option<Vec<OutPoint>>,
/// Marks a utxo as unspendable.
#[clap(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
unspendable: Option<Vec<OutPoint>>,
/// Fee rate to use in sat/vbyte.
#[clap(name = "SATS_VBYTE", short = 'f', long = "fee_rate")]
fee_rate: Option<f32>,
/// Selects which policy should be used to satisfy the external descriptor.
#[clap(name = "EXT_POLICY", long = "external_policy")]
external_policy: Option<String>,
/// Selects which policy should be used to satisfy the internal descriptor.
#[clap(name = "INT_POLICY", long = "internal_policy")]
internal_policy: Option<String>,
/// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes)
#[clap(
name = "ADD_STRING",
long = "add_string",
short = 's',
conflicts_with = "ADD_DATA"
)]
add_string: Option<String>,
/// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes)
#[clap(
name = "ADD_DATA",
long = "add_data",
short = 'o',
conflicts_with = "ADD_STRING"
)]
add_data: Option<String>, //base 64 econding
},
/// Bumps the fees of an RBF transaction.
BumpFee {
/// TXID of the transaction to update.
#[clap(name = "TXID", long = "txid")]
txid: String,
/// Allows the wallet to reduce the amount to the specified address in order to increase fees.
#[clap(name = "SHRINK_ADDRESS", long = "shrink")]
shrink_address: Option<Address>,
/// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
#[clap(long = "offline_signer")]
offline_signer: bool,
/// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used.
#[clap(name = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
utxos: Option<Vec<OutPoint>>,
/// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees.
#[clap(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
unspendable: Option<Vec<OutPoint>>,
/// The new targeted fee rate in sat/vbyte.
#[clap(name = "SATS_VBYTE", short = 'f', long = "fee_rate")]
fee_rate: f32,
},
/// Returns the available spending policies for the descriptor.
Policies,
/// Returns the public version of the wallet's descriptor(s).
PublicDescriptor,
/// Signs and tries to finalize a PSBT.
Sign {
/// Sets the PSBT to sign.
#[clap(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
/// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor.
#[clap(name = "HEIGHT", long = "assume_height")]
assume_height: Option<u32>,
/// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided.
#[clap(name = "WITNESS", long = "trust_witness_utxo")]
trust_witness_utxo: Option<bool>,
},
/// Extracts a raw transaction from a PSBT.
ExtractPsbt {
/// Sets the PSBT to extract
#[clap(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
},
/// Finalizes a PSBT.
FinalizePsbt {
/// Sets the PSBT to finalize.
#[clap(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
/// Assume the blockchain has reached a specific height.
#[clap(name = "HEIGHT", long = "assume_height")]
assume_height: Option<u32>,
/// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided.
#[clap(name = "WITNESS", long = "trust_witness_utxo")]
trust_witness_utxo: Option<bool>,
},
/// Combines multiple PSBTs into one.
CombinePsbt {
/// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT.
#[clap(name = "BASE64_PSBT", long = "psbt", required = true)]
psbt: Vec<String>,
},
}
/// Wallet subcommands that needs a blockchain backend.
#[derive(Debug, Subcommand, Clone, PartialEq)]
#[clap(rename_all = "snake")]
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "compact_filters",
feature = "rpc"
))]
pub enum OnlineWalletSubCommand {
/// Syncs with the chosen blockchain server.
Sync,
/// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract.
Broadcast {
/// Sets the PSBT to sign.
#[clap(
name = "BASE64_PSBT",
long = "psbt",
required_unless = "RAWTX",
conflicts_with = "RAWTX"
)]
psbt: Option<String>,
/// Sets the raw transaction to broadcast.
#[clap(
name = "RAWTX",
long = "tx",
required_unless = "BASE64_PSBT",
conflicts_with = "BASE64_PSBT"
)]
tx: Option<String>,
},
/// Produce a proof of reserves.
#[cfg(feature = "reserves")]
ProduceProof {
/// Sets the message.
#[clap(name = "MESSAGE", long = "message")]
msg: String,
},
/// Verify a proof of reserves for our wallet.
#[cfg(feature = "reserves")]
VerifyProof {
/// Sets the PSBT to verify.
#[clap(name = "BASE64_PSBT", long = "psbt")]
psbt: String,
/// Sets the message to verify.
#[clap(name = "MESSAGE", long = "message")]
msg: String,
/// Sets the number of block confirmations for UTXOs to be considered.
#[clap(name = "CONFIRMATIONS", long = "confirmations", default_value = "6")]
confirmations: u32,
},
}
/// Subcommands for Key operations.
#[derive(Debug, Subcommand, Clone, PartialEq)]
pub enum KeySubCommand {
/// Generates new random seed mnemonic phrase and corresponding master extended key.
Generate {
/// Entropy level based on number of random seed mnemonic words.
#[clap(
name = "WORD_COUNT",
short = 'e',
long = "entropy",
default_value = "24",
possible_values = &["12","24"],
)]
word_count: usize,
/// Seed password.
#[clap(name = "PASSWORD", short = 'p', long = "password")]
password: Option<String>,
},
/// Restore a master extended key from seed backup mnemonic words.
Restore {
/// Seed mnemonic words, must be quoted (eg. "word1 word2 ...").
#[clap(name = "MNEMONIC", short = 'm', long = "mnemonic")]
mnemonic: String,
/// Seed password.
#[clap(name = "PASSWORD", short = 'p', long = "password")]
password: Option<String>,
},
/// Derive a child key pair from a master extended key and a derivation path string (eg. "m/84'/1'/0'/0" or "m/84h/1h/0h/0").
Derive {
/// Extended private key to derive from.
#[clap(name = "XPRV", short = 'x', long = "xprv")]
xprv: ExtendedPrivKey,
/// Path to use to derive extended public key from extended private key.
#[clap(name = "PATH", short = 'p', long = "path")]
path: DerivationPath,
},
}
/// Subcommands available in REPL mode.
#[cfg(any(feature = "repl", target_arch = "wasm32"))]
#[derive(Debug, Parser, Clone, PartialEq)]
#[clap(global_settings =&[AppSettings::NoBinaryName], rename_all = "lower")]
pub enum ReplSubCommand {
/// Execute wallet commands.
Wallet {
#[clap(subcommand)]
subcommand: WalletSubCommand,
},
/// Execute key commands.
Key {
#[clap(subcommand)]
subcommand: KeySubCommand,
},
/// Execute node commands.
#[cfg(feature = "regtest-node")]
Node {
#[clap(subcommand)]
subcommand: NodeSubCommand,
},
/// Exit REPL loop.
Exit,
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(feature = "compiler")]
use crate::handlers::handle_compile_subcommand;
use crate::handlers::handle_key_subcommand;
#[cfg(all(feature = "reserves", feature = "electrum"))]
use crate::handlers::{handle_ext_reserves_subcommand, handle_online_wallet_subcommand};
use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
#[cfg(all(feature = "reserves", feature = "electrum"))]
use bdk::bitcoin::{consensus::Encodable, util::psbt::PartiallySignedTransaction};
use bdk::bitcoin::{Address, Network, OutPoint};
use bdk::miniscript::bitcoin::network::constants::Network::Testnet;
#[cfg(all(feature = "reserves", feature = "electrum"))]
use bdk::{
blockchain::ElectrumBlockchain, database::MemoryDatabase, electrum_client::Client,
SyncOptions, Wallet,
};
use std::str::{self, FromStr};
use super::OfflineWalletSubCommand::{BumpFee, CreateTx, GetNewAddress};
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "compact_filters",
feature = "rpc"
))]
use super::OnlineWalletSubCommand::{Broadcast, Sync};
use super::WalletSubCommand::OfflineWalletSubCommand;
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "compact_filters",
feature = "rpc"
))]
use super::WalletSubCommand::OnlineWalletSubCommand;
#[cfg(feature = "repl")]
use regex::Regex;
#[test]
fn test_clap_args() {
use clap::CommandFactory;
CliOpts::command().debug_assert();
}
#[test]
fn test_parse_wallet_get_new_address() {
let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"get_new_address"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
#[cfg(feature = "electrum")]
electrum_opts: ElectrumOpts {
timeout: None,
server: "ssl://electrum.blockstream.info:60002".to_string(),
stop_gap: 10,
},
#[cfg(feature = "esplora")]
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
#[cfg(feature = "compact_filters")]
compactfilter_opts: CompactFilterOpts{
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 0,
},
#[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
},
#[cfg(feature = "rpc")]
rpc_opts: RpcOpts {
address: "127.0.0.1:18443".to_string(),
basic_auth: ("user".to_string(), "password".to_string()),
cookie: None,
start_time: 0,
},
},
subcommand: OfflineWalletSubCommand(GetNewAddress),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(feature = "electrum")]
#[test]
fn test_parse_wallet_electrum() {
let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
"--proxy", "127.0.0.1:9150", "--retries", "3", "--timeout", "10",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--server","ssl://electrum.blockstream.info:50002",
"--stop_gap", "20",
"get_new_address"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Testnet,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
electrum_opts: ElectrumOpts {
timeout: Some(10),
server: "ssl://electrum.blockstream.info:50002".to_string(),
stop_gap: 20
},
proxy_opts: ProxyOpts{
proxy: Some("127.0.0.1:9150".to_string()),
proxy_auth: None,
retries: 3,
},
},
subcommand: OfflineWalletSubCommand(GetNewAddress),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(feature = "esplora-ureq")]
#[test]
fn test_parse_wallet_esplora() {
let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--server", "https://blockstream.info/api/",
"--timeout", "10",
"--stop_gap", "20",
"get_new_address"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
esplora_opts: EsploraOpts {
server: "https://blockstream.info/api/".to_string(),
timeout: 10,
stop_gap: 20,
conc: 4,
},
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
}
},
subcommand: OfflineWalletSubCommand(GetNewAddress),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(feature = "esplora-reqwest")]
#[test]
fn test_parse_wallet_esplora() {
let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--server", "https://blockstream.info/api/",
"--conc", "10",
"--stop_gap", "20",
"get_new_address"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
esplora_opts: EsploraOpts {
server: "https://blockstream.info/api/".to_string(),
conc: 10,
stop_gap: 20,
timeout: 5,
},
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
}
},
subcommand: OfflineWalletSubCommand(GetNewAddress),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(feature = "rpc")]
#[test]
fn test_parse_wallet_rpc() {
let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--node", "125.67.89.101:56678",
"--basic-auth", "user:password",
"--cookie", "/home/user/.bitcoin/regtest/.cookie",
"--start-time", "123456",
"get_new_address"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
rpc_opts: RpcOpts {
address: "125.67.89.101:56678".to_string(),
basic_auth: ("user".to_string(), "password".to_string()),
cookie: Some("/home/user/.bitcoin/regtest/.cookie".to_string()),
start_time: 123456,
},
},
subcommand: OfflineWalletSubCommand(GetNewAddress),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(feature = "compact_filters")]
#[test]
fn test_parse_wallet_compact_filters() {
let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--proxy", "127.0.0.1:9005",
"--proxy_auth", "random_user:random_passwd",
"--node", "127.0.0.1:18444",
"--conn_count", "4",
"--skip_blocks", "5",
"get_new_address"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
compactfilter_opts: CompactFilterOpts{
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 5,
},
proxy_opts: ProxyOpts{
proxy: Some("127.0.0.1:9005".to_string()),
proxy_auth: Some(("random_user".to_string(), "random_passwd".to_string())),
retries: 5,
}
},
subcommand: OfflineWalletSubCommand(GetNewAddress),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "compact_filters",
feature = "rpc"
))]
#[test]
fn test_parse_wallet_sync() {
let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"sync"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Testnet,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: None,
#[cfg(feature = "electrum")]
electrum_opts: ElectrumOpts {
timeout: None,
server: "ssl://electrum.blockstream.info:60002".to_string(),
stop_gap: 10,
},
#[cfg(feature = "esplora")]
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
#[cfg(feature = "compact_filters")]
compactfilter_opts: CompactFilterOpts{
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 0,
},
#[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
},
#[cfg(feature = "rpc")]
rpc_opts: RpcOpts {
address: "127.0.0.1:18443".to_string(),
basic_auth: ("user".to_string(), "password".to_string()),
cookie: None,
start_time: 0,
},
},
subcommand: OnlineWalletSubCommand(Sync),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[test]
fn test_parse_wallet_create_tx() {
let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456", "--to", "mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910",
"--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
"--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
"--add_string","Hello BDK",
];
let cli_opts = CliOpts::parse_from(&cli_args);
let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ")
.unwrap()
.script_pubkey();
let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf")
.unwrap()
.script_pubkey();
let outpoint1 = OutPoint::from_str(
"87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
)
.unwrap();
let outpoint2 = OutPoint::from_str(
"87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
)
.unwrap();
let expected_cli_opts = CliOpts {
network: Network::Testnet,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
#[cfg(feature = "electrum")]
electrum_opts: ElectrumOpts {
timeout: None,
server: "ssl://electrum.blockstream.info:60002".to_string(),
stop_gap: 10,
},
#[cfg(feature = "esplora")]
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
#[cfg(feature = "compact_filters")]
compactfilter_opts: CompactFilterOpts{
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 0,
},
#[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
},
#[cfg(feature = "rpc")]
rpc_opts: RpcOpts {
address: "127.0.0.1:18443".to_string(),
basic_auth: ("user".to_string(), "password".to_string()),
cookie: None,
start_time: 0,
},
},
subcommand: WalletSubCommand::OfflineWalletSubCommand(CreateTx {
recipients: vec![(script1, 123456), (script2, 78910)],
send_all: false,
enable_rbf: false,
offline_signer: false,
utxos: Some(vec!(outpoint1, outpoint2)),
unspendable: None,
fee_rate: None,
external_policy: None,
internal_policy: None,
add_data: None,
add_string: Some("Hello BDK".to_string()),
}),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[test]
fn test_parse_wallet_bump_fee() {
let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"bump_fee", "--fee_rate", "6.1",
"--txid","35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506",
"--shrink","tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Testnet,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
#[cfg(feature = "electrum")]
electrum_opts: ElectrumOpts {
timeout: None,
server: "ssl://electrum.blockstream.info:60002".to_string(),
stop_gap: 10,
},
#[cfg(feature = "esplora")]
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
#[cfg(feature = "compact_filters")]
compactfilter_opts: CompactFilterOpts{
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 0,
},
#[cfg(feature = "rpc")]
rpc_opts: RpcOpts {
address: "127.0.0.1:18443".to_string(),
basic_auth: ("user".to_string(), "password".to_string()),
cookie: None,
start_time: 0,
},
#[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
}
},
subcommand: OfflineWalletSubCommand(BumpFee {
txid: "35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506".to_string(),
shrink_address: Some(Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap()),
offline_signer: false,
utxos: None,
unspendable: None,
fee_rate: 6.1
}),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(any(
feature = "electrum",
feature = "esplora",
feature = "compact_filters",
feature = "rpc"
))]
#[test]
fn test_parse_wallet_broadcast() {
let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"broadcast",
"--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Testnet,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
change_descriptor: None,
#[cfg(feature = "electrum")]
electrum_opts: ElectrumOpts {
timeout: None,
server: "ssl://electrum.blockstream.info:60002".to_string(),
stop_gap: 10,
},
#[cfg(feature = "esplora")]
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
#[cfg(feature = "compact_filters")]
compactfilter_opts: CompactFilterOpts{
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 0,
},
#[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
proxy_opts: ProxyOpts{
proxy: None,
proxy_auth: None,
retries: 5,
},
#[cfg(feature = "rpc")]
rpc_opts: RpcOpts {
address: "127.0.0.1:18443".to_string(),
basic_auth: ("user".to_string(), "password".to_string()),
cookie: None,
start_time: 0,
},
},
subcommand: OnlineWalletSubCommand(Broadcast {
psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()),
tx: None
}),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[test]
fn test_parse_wrong_network() {
let cli_args = vec!["repl", "--network", "badnet", "wallet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"sync"];
let cli_opts = CliOpts::from_iter_safe(&cli_args);
assert!(cli_opts.is_err());
}
#[test]
fn test_key_generate() {
let network = Testnet;
let key_generate_cmd = KeySubCommand::Generate {
word_count: 12,
password: Some("test123".to_string()),
};
let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
let result_obj = result.as_object().unwrap();
let mnemonic = result_obj.get("mnemonic").unwrap().as_str().unwrap();
let mnemonic: Vec<&str> = mnemonic.split(' ').collect();
let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
assert_eq!(mnemonic.len(), 12);
assert_eq!(&xprv[0..4], "tprv");
}
#[test]
fn test_key_restore() {
let network = Testnet;
let key_generate_cmd = KeySubCommand::Restore {
mnemonic: "payment battle unit sword token broccoli era violin purse trip blood hire"
.to_string(),
password: Some("test123".to_string()),
};
let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
let result_obj = result.as_object().unwrap();
let fingerprint = result_obj.get("fingerprint").unwrap().as_str().unwrap();
let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
assert_eq!(&fingerprint, &"828af366");
assert_eq!(&xprv, &"tprv8ZgxMBicQKsPd18TeiFknZKqaZFwpdX9tvvKh8eeHSSPBQi5g9xPHztBg411o78G8XkrhQb6Q1cVvBJ1a9xuFHpmWgvQsvkJkNxBjfGoqhK");
}
#[test]
fn test_key_derive() {
let network = Testnet;
let key_generate_cmd = KeySubCommand::Derive {
xprv: ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPfQjJy8ge2cvBfDjLxJSkvNLVQiw7BQ5gTjKadG2rrcQB5zjcdaaUTz5EDNJaS77q4DzjqjogQBfMsaXFFNP3UqoBnwt2kyT").unwrap(),
path: DerivationPath::from_str("m/84'/1'/0'/0").unwrap(),
};
let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
let result_obj = result.as_object().unwrap();
let xpub = result_obj.get("xpub").unwrap().as_str().unwrap();
let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
assert_eq!(&xpub, &"[566844c5/84'/1'/0'/0]tpubDFeqiDkfwR1tAhPxsXSZMfEmfpDhwhLyhLKZgmeBvuBkZQusoWeL62oGg2oTNGcENeKdwuGepAB85eMvyLemabYe9PSqv6cr5mFXktHc3Ka/*");
assert_eq!(&xprv, &"[566844c5/84'/1'/0'/0]tprv8ixoZoiRo3LDHENAysmxxFaf6nhmnNA582inQFbtWdPMivf7B7pjuYBQVuLC5bkM7tJZEDbfoivENsGZPBnQg1n52Kuc1P8X2Ei3XJuJX7c/*");
}
#[cfg(feature = "compiler")]
#[test]
fn test_parse_compile() {
let cli_args = vec![
"bdk-cli",
"compile",
"thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))",
"--type",
"sh-wsh",
];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Testnet,
datadir: None,
subcommand: CliSubCommand::Compile {
policy: "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(),
script_type: "sh-wsh".to_string(),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(feature = "compiler")]
#[test]
fn test_compile() {
let result = handle_compile_subcommand(
Network::Testnet,
"thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(),
"sh-wsh".to_string(),
)
.unwrap();
let result_obj = result.as_object().unwrap();
let descriptor = result_obj.get("descriptor").unwrap().as_str().unwrap();
assert_eq!(
&descriptor,
&"sh(wsh(thresh(3,pk(Alice),s:pk(Bob),s:pk(Carol),snl:older(2))))#rmef3s78"
);
}
#[cfg(all(feature = "reserves", feature = "compact_filters"))]
#[test]
fn test_parse_produce_proof() {
let message = "Those coins belong to Satoshi Nakamoto";
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"wallet",
"--descriptor",
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
"produce_proof",
"--message",
message.clone(),
];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
.to_string(),
change_descriptor: None,
compactfilter_opts: CompactFilterOpts {
address: vec!["127.0.0.1:18444".to_string()],
conn_count: 4,
skip_blocks: 0,
},
proxy_opts: ProxyOpts {
proxy: None,
proxy_auth: None,
retries: 5,
},
},
subcommand: OnlineWalletSubCommand(ProduceProof {
msg: message.to_string(),
}),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
#[test]
fn test_parse_verify_proof_internal() {
let psbt = r#"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"#;
let message = "Those coins belong to Satoshi Nakamoto";
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"wallet",
"--descriptor",
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
"verify_proof",
"--psbt",
psbt.clone(),
"--message",
message.clone(),
];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
.to_string(),
change_descriptor: None,
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
proxy_opts: ProxyOpts {
proxy: None,
proxy_auth: None,
retries: 5,
},
},
subcommand: OnlineWalletSubCommand(OnlineWalletSubCommand::VerifyProof {
psbt: psbt.to_string(),
msg: message.to_string(),
confirmations: 6,
}),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
#[test]
fn test_parse_verify_proof_internal_confirmation() {
let psbt = r#"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"#;
let message = "Those coins belong to Satoshi Nakamoto";
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"wallet",
"--descriptor",
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
"verify_proof",
"--psbt",
psbt.clone(),
"--message",
message.clone(),
"--confirmations",
"0",
];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::Wallet {
wallet_opts: WalletOpts {
wallet: None,
verbose: false,
descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
.to_string(),
change_descriptor: None,
esplora_opts: EsploraOpts {
server: "https://blockstream.info/testnet/api/".to_string(),
timeout: 5,
stop_gap: 10,
conc: 4,
},
proxy_opts: ProxyOpts {
proxy: None,
proxy_auth: None,
retries: 5,
},
},
subcommand: OnlineWalletSubCommand(OnlineWalletSubCommand::VerifyProof {
psbt: psbt.to_string(),
msg: message.to_string(),
confirmations: 0,
}),
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
#[cfg(all(feature = "reserves", feature = "electrum"))]
#[test]
fn test_parse_verify_proof_external() {
let psbt = r#"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"#.to_string();
let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d".to_string();
let message = "Those coins belong to Satoshi Nakamoto".to_string();
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"external_reserves",
&message,
&psbt,
"6",
&address,
"--server",
"ssl://electrum.blockstream.info:60002",
];
let cli_opts = CliOpts::from_iter(&cli_args);
let expected_cli_opts = CliOpts {
network: Network::Bitcoin,
datadir: None,
subcommand: CliSubCommand::ExternalReserves {
message,
psbt,
confirmations: 6,
addresses: [address].to_vec(),
electrum_opts: ElectrumOpts {
timeout: None,
server: "ssl://electrum.blockstream.info:60002".to_string(),
stop_gap: 10,
},
},
};
assert_eq!(expected_cli_opts, cli_opts);
}
/// Encodes a partially signed transaction as base64 and returns the bytes of the resulting string.
#[cfg(all(feature = "reserves", feature = "electrum"))]
fn encode_psbt(psbt: PartiallySignedTransaction) -> Vec<u8> {
let mut encoded = Vec::<u8>::new();
psbt.consensus_encode(&mut encoded).unwrap();
let base64_psbt = base64::encode(&encoded);
base64_psbt.as_bytes().to_vec()
}
#[cfg(all(feature = "reserves", feature = "electrum"))]
#[test]
fn test_proof_of_reserves_wallet() {
let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string();
let message = "Those coins belong to Satoshi Nakamoto";
let client = Client::new("ssl://electrum.blockstream.info:60002").unwrap();
let blockchain = ElectrumBlockchain::from(client);
let wallet = Wallet::new(
&descriptor,
None,
Network::Testnet,
MemoryDatabase::default(),
)
.unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let balance = wallet.get_balance().unwrap();
let addr = wallet.get_address(bdk::wallet::AddressIndex::New).unwrap();
assert_eq!(
"tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d",
addr.to_string()
);
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"wallet",
"--descriptor",
&descriptor,
"produce_proof",
"--message",
message.clone(),
];
let cli_opts = CliOpts::from_iter(&cli_args);
let wallet_subcmd = match cli_opts.subcommand {
CliSubCommand::Wallet {
wallet_opts: _,
subcommand: OnlineWalletSubCommand(online_subcommand),
} => online_subcommand,
_ => panic!("unexpected subcommand"),
};
let result = handle_online_wallet_subcommand(&wallet, &blockchain, wallet_subcmd).unwrap();
let psbt: PartiallySignedTransaction =
serde_json::from_str(&result.as_object().unwrap().get("psbt").unwrap().to_string())
.unwrap();
let psbt = encode_psbt(psbt);
let psbt = str::from_utf8(&psbt).unwrap();
assert_eq!(format!("{}", psbt), "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");
let psbt_b64 = &result
.as_object()
.unwrap()
.get("psbt_base64")
.unwrap()
.to_string();
assert_eq!(&format!("{}", psbt), psbt_b64.trim_matches('\"'));
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"wallet",
"--descriptor",
&descriptor,
"verify_proof",
"--psbt",
psbt,
"--message",
message.clone(),
];
let cli_opts = CliOpts::from_iter(&cli_args);
let wallet_subcmd = match cli_opts.subcommand {
CliSubCommand::Wallet {
wallet_opts: _,
subcommand: OnlineWalletSubCommand(online_subcommand),
} => online_subcommand,
_ => panic!("unexpected subcommand"),
};
let result = handle_online_wallet_subcommand(&wallet, &blockchain, wallet_subcmd).unwrap();
let spendable = result
.as_object()
.unwrap()
.get("spendable")
.unwrap()
.as_u64()
.unwrap();
assert_eq!(spendable, balance.get_spendable());
}
#[cfg(all(feature = "reserves", feature = "electrum"))]
#[test]
fn test_proof_of_reserves_veryfy() {
let message = "Those coins belong to Satoshi Nakamoto";
let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d";
let psbt = "cHNidP8BAKcBAAAAA9A7AuVMuwIrkAuXtKDqw3BruovK2PH7E90MzCbyaAMlAAAAAAD/////sq+79fxJ+5F8951Xcbp8N5VYVJoesG1zd3AxStcGwPYAAAAAAP/////AUS4n/SkO9SSvIv8MI2Xdm3xdn77Sud6uX5PyqCganAEAAAAA/////wGwrQEAAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQcAAAEBHxAnAAAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40gwRQIhAPgByvkajQrNeQDSGik2gnxpo/P/owiEHR+0nWefkXurAiBgrAlDvwuTiaGEEWQW/Kd7L7u7YOQnqvrd46DR0A8yPgEBBwABCGwCSDBFAiEA+AHK+RqNCs15ANIaKTaCfGmj8/+jCIQdH7SdZ5+Re6sCIGCsCUO/C5OJoYQRZBb8p3svu7tg5Ceq+t3joNHQDzI+ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfoIYBAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiBSfiX0qP7vR+2Qx/mRJS8pwma8nTfOWKerzo6c0iSAfwIgEfX4Wt7YXd8MkKUEY627GWYCmKfMsJGcIC0U1wgc1vUBAQcAAQhrAkcwRAIgUn4l9Kj+70ftkMf5kSUvKcJmvJ03zlinq86OnNIkgH8CIBH1+Fre2F3fDJClBGOtuxlmApinzLCRnCAtFNcIHNb1ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA==";
let cli_args = vec![
"bdk-cli",
"--network",
"bitcoin",
"external_reserves",
message,
psbt,
"6",
address,
address, // passing the address twice on purpose, to test passing of multiple addresses
"--server",
"ssl://electrum.blockstream.info:60002",
];
let cli_opts = CliOpts::from_iter(&cli_args);
let (message, psbt, confirmations, addresses, electrum_opts) = match cli_opts.subcommand {
CliSubCommand::ExternalReserves {
message,
psbt,
confirmations,
addresses,
electrum_opts,
} => (message, psbt, confirmations, addresses, electrum_opts),
_ => panic!("unexpected subcommand"),
};
let result = handle_ext_reserves_subcommand(
Network::Bitcoin,
message,
psbt,
confirmations,
addresses,
electrum_opts,
)
.unwrap();
let spendable = result
.as_object()
.unwrap()
.get("spendable")
.unwrap()
.as_u64()
.unwrap();
assert!(spendable > 0);
}
#[cfg(feature = "repl")]
#[test]
fn test_regex_double_quotes() {
let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX).unwrap();
let line = r#"restore -m "word1 word2 word3" -p 'test! 123 -test' "#;
let split_line: Vec<&str> = split_regex
.captures_iter(&line)
.map(|c| {
c.get(1)
.or_else(|| c.get(2))
.or_else(|| c.get(3))
.unwrap()
.as_str()
})
.collect();
assert_eq!(
vec!(
"restore",
"-m",
"word1 word2 word3",
"-p",
"test! 123 -test"
),
split_line
);
}
#[cfg(feature = "repl")]
#[test]
fn test_regex_single_quotes() {
let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX).unwrap();
let line = r#"restore -m 'word1 word2 word3' -p "test *123 -test" "#;
let split_line: Vec<&str> = split_regex
.captures_iter(&line)
.map(|c| {
c.get(1)
.or_else(|| c.get(2))
.or_else(|| c.get(3))
.unwrap()
.as_str()
})
.collect();
assert_eq!(
vec!(
"restore",
"-m",
"word1 word2 word3",
"-p",
"test *123 -test"
),
split_line
);
}
}