cargo run --features rpc -- wallet -w regtest1 balance
```
-## Formatting Responses using `--pretty` flag
-
-You can optionally return outputs of commands in human-readable, tabular format instead of `JSON`. To enable this option, simply add the `--pretty` flag as a top level flag. For instance, you wallet's balance in a pretty format, you can run:
-
-```shell
-cargo run -- --pretty -n signet wallet -w {wallet_name} balance
-```
-This is available for wallet, key, repl and compile features. When ommitted, outputs default to `JSON`.
-
## Shell Completions
`bdk-cli` supports generating shell completions for Bash, Zsh, Fish, Elvish, and PowerShell. For setup instructions, run:
//! All subcommands are defined in the below enums.
#![allow(clippy::large_enum_variant)]
-use crate::{
- error::BDKCliError as Error,
- handlers::{
- AppCommand,
- offline::{
- BalanceCommand, BumpFeeCommand, CombinePsbtCommand, CreateTxCommand,
- ExtractPsbtCommand, FinalizePsbtCommand, NewAddressCommand, PoliciesCommand,
- PublicDescriptorCommand, SignCommand, SignMessageCommand, TransactionsCommand,
- UnspentCommand, UnusedAddressCommand, VerifyMessageCommand,
- },
- online::{
- BroadcastCommand, FullScanCommand, ReceivePayjoinCommand, SendPayjoinCommand,
- SyncCommand,
- },
+#[cfg(feature = "bip322")]
+use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand};
+use crate::handlers::{
+ config::{ListWalletsCommand, SaveConfigCommand},
+ descriptor::DescriptorCommand,
+ key::{DeriveKeyCommand, GenerateKeyCommand, RestoreKeyCommand},
+ offline::{
+ BalanceCommand, BumpFeeCommand, CombinePsbtCommand, CreateTxCommand, ExtractPsbtCommand,
+ FinalizePsbtCommand, NewAddressCommand, PoliciesCommand, PublicDescriptorCommand,
+ SignCommand, TransactionsCommand, UnspentCommand, UnusedAddressCommand,
+ },
+ online::{
+ BroadcastCommand, FullScanCommand, ReceivePayjoinCommand, SendPayjoinCommand, SyncCommand,
},
- utils::output::FormatOutput,
-};
-use bdk_wallet::bitcoin::{
- Address, Network, OutPoint, ScriptBuf,
- bip32::{DerivationPath, Xpriv},
};
-use clap::{Args, Parser, Subcommand, ValueEnum, value_parser};
-use clap_complete::Shell;
-use crate::{
- handlers::AppContext,
- utils::{parse_address, parse_outpoint, parse_recipient},
-};
+#[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+))]
+use crate::client::ClientType;
+
+#[cfg(feature = "compiler")]
+use crate::handlers::descriptor::CompileCommand;
+#[cfg(any(feature = "sqlite", feature = "redb"))]
+use crate::persister::DatabaseType;
+
+use bdk_wallet::bitcoin::Network;
+use clap::{Args, Parser, Subcommand, value_parser};
+use clap_complete::Shell;
// #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
use crate::utils::parse_proxy_auth;
/// Default value : ~/.bdk-bitcoin
#[arg(env = "DATADIR", short = 'd', long = "datadir")]
pub datadir: Option<std::path::PathBuf>,
- /// Output results in pretty format (instead of JSON).
- #[arg(long = "pretty", global = true)]
- pub pretty: bool,
/// Top level cli sub-commands.
#[command(subcommand)]
pub subcommand: CliSubCommand,
subcommand: KeySubCommand,
},
/// Compile a miniscript policy to an output descriptor.
- // #[cfg(feature = "compiler")]
+ #[cfg(feature = "compiler")]
#[clap(long_about = "Miniscript policy compiler")]
- Compile {
- /// Sets the spending policy to compile.
- #[arg(env = "POLICY", required = true, index = 1)]
- policy: String,
- /// Sets the script type used to embed the compiled policy.
- #[arg(env = "TYPE", short = 't', long = "type", default_value = "wsh", value_parser = ["sh","wsh", "sh-wsh", "tr"]
- )]
- script_type: String,
- },
+ Compile(CompileCommand),
// #[cfg(feature = "repl")]
/// REPL command loop mode.
///
#[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)]
wallet: String,
},
+
/// Output Descriptors operations.
///
/// Generate output descriptors from either extended key (Xprv/Xpub) or mnemonic phrase.
/// This feature is intended for development and testing purposes only.
- Descriptor {
- /// Descriptor type (script type)
- #[arg(
- long = "type",
- short = 't',
- value_parser = ["pkh", "wpkh", "sh", "wsh", "tr"],
- default_value = "wsh"
- )]
- desc_type: String,
- /// Optional key: xprv, xpub, or mnemonic phrase
- key: Option<String>,
- },
+ Descriptor(DescriptorCommand),
+
/// List all saved wallet configurations.
- Wallets,
+ Wallets(ListWalletsCommand),
/// Generate tab-completion scripts for your shell.
///
/// The completion script is output on stdout, allowing you to redirect
},
}
-impl CliSubCommand {
- pub async fn execute(
- &self,
- ctx: &mut AppContext<'_>,
- cli_opts: &CliOpts,
- #[cfg(feature = "repl")] wallet_opts: &mut WalletOpts,
- datadir: std::path::PathBuf,
- ) -> Result<String, Error> {
- match self {
- CliSubCommand::Wallet { subcommand, wallet } => {
- match subcommand {
- WalletSubCommand::OfflineWalletSubCommand(cmd) => {
- cmd.execute(ctx, cli_opts.pretty)
- }
- // #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf", feature = "rpc"))]
- WalletSubCommand::OnlineWalletSubCommand(cmd) => {
- cmd.execute(ctx, cli_opts.pretty).await
- }
- WalletSubCommand::Config { force, wallet_opts } => {}
- }
- }
-
- CliSubCommand::Key { subcommand } => subcommand.execute(ctx, cli_opts.pretty),
-
- CliSubCommand::Descriptor { desc_type, key } => {
- cmd.execute(ctx).and_then(|out| out.format(cli_opts.pretty))
- }
-
- // #[cfg(feature = "compiler")]
- CliSubCommand::Compile {
- policy,
- script_type,
- } => cmd.execute(ctx).and_then(|out| out.format(cli_opts.pretty)),
-
- CliSubCommand::Wallets(cmd) => {
- cmd.execute(ctx).and_then(|out| out.format(cli_opts.pretty))
- }
-
- CliSubCommand::Config(cmd) => {
- cmd.execute(ctx).and_then(|out| out.format(cli_opts.pretty))
- }
-
- CliSubCommand::Completions { shell } => {
- use clap::CommandFactory;
- clap_complete::generate(
- *shell,
- &mut CliOpts::command(),
- "bdk-cli",
- &mut std::io::stdout(),
- );
- Ok("".to_string())
- }
- }
- }
-}
-
/// Wallet operation subcommands.
#[derive(Debug, Subcommand, Clone, PartialEq)]
pub enum WalletSubCommand {
/// Save wallet configuration to `config.toml`.
- Config {
- /// Overwrite existing wallet configuration if it exists.
- #[arg(short = 'f', long = "force", default_value_t = false)]
- force: bool,
-
- #[command(flatten)]
- wallet_opts: WalletOpts,
- },
- // #[cfg(any(
- // feature = "electrum",
- // feature = "esplora",
- // feature = "cbf",
- // feature = "rpc"
- // ))]
+ Config(SaveConfigCommand),
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+ ))]
#[command(flatten)]
OnlineWalletSubCommand(OnlineWalletSubCommand),
#[command(flatten)]
OfflineWalletSubCommand(OfflineWalletSubCommand),
}
-#[derive(Clone, ValueEnum, Debug, Eq, PartialEq)]
-pub enum DatabaseType {
- /// Sqlite database
- #[cfg(feature = "sqlite")]
- Sqlite,
- /// Redb database
- #[cfg(feature = "redb")]
- Redb,
-}
-
-// #[cfg(any(
-// feature = "electrum",
-// feature = "esplora",
-// feature = "rpc",
-// feature = "cbf"
-// ))]
-#[derive(Clone, ValueEnum, Debug, Eq, PartialEq)]
-pub enum ClientType {
- // #[cfg(feature = "electrum")]
- Electrum,
- // #[cfg(feature = "esplora")]
- Esplora,
- // #[cfg(feature = "rpc")]
- Rpc,
- // #[cfg(feature = "cbf")]
- Cbf,
-}
-
/// Config options wallet operations can take.
#[derive(Debug, Args, Clone, PartialEq, Eq)]
pub struct WalletOpts {
/// Sets the descriptor to use for internal/change addresses.
#[arg(env = "INT_DESCRIPTOR", short = 'i', long)]
pub int_descriptor: Option<String>,
- // #[cfg(any(
- // feature = "electrum",
- // feature = "esplora",
- // feature = "rpc",
- // feature = "cbf"
- // ))]
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+ ))]
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum, required = true)]
pub client_type: ClientType,
#[cfg(any(feature = "sqlite", feature = "redb"))]
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, required = true)]
pub database_type: DatabaseType,
/// Sets the server url.
- // #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+ #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
#[arg(env = "SERVER_URL", short = 'u', long, required = true)]
pub url: String,
/// Electrum batch size.
- // #[cfg(feature = "electrum")]
+ #[cfg(feature = "electrum")]
#[arg(env = "ELECTRUM_BATCH_SIZE", short = 'b', long, default_value = "10")]
pub batch_size: usize,
/// Esplora parallel requests.
- // #[cfg(feature = "esplora")]
+ #[cfg(feature = "esplora")]
#[arg(
env = "ESPLORA_PARALLEL_REQUESTS",
short = 'p',
default_value = "5"
)]
pub parallel_requests: usize,
- // #[cfg(feature = "rpc")]
+ #[cfg(feature = "rpc")]
/// Sets the rpc basic authentication.
#[arg(
env = "USER:PASSWD",
default_value = "user:password",
)]
pub basic_auth: (String, String),
- // #[cfg(feature = "rpc")]
+ #[cfg(feature = "rpc")]
/// Sets an optional cookie authentication.
#[arg(env = "COOKIE")]
pub cookie: Option<String>,
- // #[cfg(feature = "cbf")]
+ #[cfg(feature = "cbf")]
#[clap(flatten)]
pub compactfilter_opts: CompactFilterOpts,
}
Balance(BalanceCommand),
/// Creates a new unsigned transaction.
CreateTx(CreateTxCommand),
- // 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.
- // #[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)]
- // recipients: Vec<(ScriptBuf, u64)>,
- // /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
- // #[arg(long = "send_all", short = 'a')]
- // send_all: bool,
- // /// Enables Replace-By-Fee (BIP125).
- // #[arg(long = "enable_rbf", short = 'r', default_value_t = true)]
- // 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.
- // #[arg(long = "offline_signer")]
- // offline_signer: bool,
- // /// Selects which utxos *must* be spent.
- // #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)]
- // utxos: Option<Vec<OutPoint>>,
- // /// Marks a utxo as unspendable.
- // #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
- // unspendable: Option<Vec<OutPoint>>,
- // /// Fee rate to use in sat/vbyte.
- // #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
- // fee_rate: Option<f32>,
- // /// Selects which policy should be used to satisfy the external descriptor.
- // #[arg(env = "EXT_POLICY", long = "external_policy")]
- // external_policy: Option<String>,
- // /// Selects which policy should be used to satisfy the internal descriptor.
- // #[arg(env = "INT_POLICY", long = "internal_policy")]
- // internal_policy: Option<String>,
- // /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes)
- // #[arg(
- // env = "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)
- // #[arg(
- // env = "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(BumpFeeCommand),
- // BumpFee {
- // /// TXID of the transaction to update.
- // #[arg(env = "TXID", long = "txid")]
- // txid: String,
- // /// Allows the wallet to reduce the amount to the specified address in order to increase fees.
- // #[arg(env = "SHRINK_ADDRESS", long = "shrink", value_parser = parse_address)]
- // 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.
- // #[arg(long = "offline_signer")]
- // offline_signer: bool,
- // /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used.
- // #[arg(env = "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.
- // #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
- // unspendable: Option<Vec<OutPoint>>,
- // /// The new targeted fee rate in sat/vbyte.
- // #[arg(
- // env = "SATS_VBYTE",
- // short = 'f',
- // long = "fee_rate",
- // default_value = "1.0"
- // )]
- // fee_rate: f32,
- // },
/// Returns the available spending policies for the descriptor.
Policies(PoliciesCommand),
/// Returns the public version of the wallet's descriptor(s).
PublicDescriptor(PublicDescriptorCommand),
/// Signs and tries to finalize a PSBT.
Sign(SignCommand),
- // Sign {
- // /// Sets the PSBT to sign.
- // #[arg(env = "BASE64_PSBT")]
- // psbt: String,
- // /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor.
- // #[arg(env = "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.
- // #[arg(env = "WITNESS", long = "trust_witness_utxo")]
- // trust_witness_utxo: Option<bool>,
- // },
+
/// Extracts a raw transaction from a PSBT.
- // ExtractPsbt {
- // /// Sets the PSBT to extract
- // #[arg(env = "BASE64_PSBT")]
- // psbt: String,
- // },
ExtractPsbt(ExtractPsbtCommand),
/// Finalizes a PSBT.
- // FinalizePsbt {
- // /// Sets the PSBT to finalize.
- // #[arg(env = "BASE64_PSBT")]
- // psbt: String,
- // /// Assume the blockchain has reached a specific height.
- // #[arg(env = "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.
- // #[arg(env = "WITNESS", long = "trust_witness_utxo")]
- // trust_witness_utxo: Option<bool>,
- // },
FinalizePsbt(FinalizePsbtCommand),
/// Combines multiple PSBTs into one.
- // CombinePsbt {
- // /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT.
- // #[arg(env = "BASE64_PSBT", required = true)]
- // psbt: Vec<String>,
- // },
CombinePsbt(CombinePsbtCommand),
/// Sign a message using BIP322
- // #[cfg(feature = "bip322")]
+ #[cfg(feature = "bip322")]
// SignMessage {
// /// The message to sign
// #[arg(long)]
// },
SignMessage(SignMessageCommand),
/// Verify a BIP322 signature
- // #[cfg(feature = "bip322")]
+ #[cfg(feature = "bip322")]
// VerifyMessage {
// /// The signature proof to verify
// #[arg(long)]
// ))]
pub enum OnlineWalletSubCommand {
/// Full Scan with the chosen blockchain server.
- // FullScan {
- // /// Stop searching addresses for transactions after finding an unused gap of this length.
- // #[arg(env = "STOP_GAP", long = "scan-stop-gap", default_value = "20")]
- // stop_gap: usize,
- // },
FullScan(FullScanCommand),
/// Syncs with the chosen blockchain server.
Sync(SyncCommand),
/// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract.
- // Broadcast {
- // /// Sets the PSBT to sign.
- // #[arg(
- // env = "BASE64_PSBT",
- // long = "psbt",
- // required_unless_present = "tx",
- // conflicts_with = "tx"
- // )]
- // psbt: Option<String>,
- // /// Sets the raw transaction to broadcast.
- // #[arg(
- // env = "RAWTX",
- // long = "tx",
- // required_unless_present = "psbt",
- // conflicts_with = "psbt"
- // )]
- // tx: Option<String>,
- // },
Broadcast(BroadcastCommand),
/// Generates a Payjoin receive URI and processes the sender's Payjoin proposal.
// ReceivePayjoin {
#[derive(Debug, Subcommand, Clone, PartialEq, Eq)]
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.
- #[arg(
- env = "WORD_COUNT",
- short = 'e',
- long = "entropy",
- default_value = "12"
- )]
- word_count: usize,
- /// Seed password.
- #[arg(env = "PASSWORD", short = 'p', long = "password")]
- password: Option<String>,
- },
+ Generate(GenerateKeyCommand),
/// Restore a master extended key from seed backup mnemonic words.
- Restore {
- /// Seed mnemonic words, must be quoted (eg. "word1 word2 ...").
- #[arg(env = "MNEMONIC", short = 'm', long = "mnemonic")]
- mnemonic: String,
- /// Seed password.
- #[arg(env = "PASSWORD", short = 'p', long = "password")]
- password: Option<String>,
- },
+ Restore(RestoreKeyCommand),
/// 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.
- #[arg(env = "XPRV", short = 'x', long = "xprv")]
- xprv: Xpriv,
- /// Path to use to derive extended public key from extended private key.
- #[arg(env = "PATH", short = 'p', long = "path")]
- path: DerivationPath,
- },
+ Derive(DeriveKeyCommand),
}
/// Subcommands available in REPL mode.
subcommand: KeySubCommand,
},
/// Generate descriptors
- Descriptor {
- /// Descriptor type (script type).
- #[arg(
- long = "type",
- short = 't',
- value_parser = ["pkh", "wpkh", "sh", "wsh", "tr"],
- default_value = "wsh"
- )]
- desc_type: String,
- /// Optional key: xprv, xpub, or mnemonic phrase
- key: Option<String>,
- },
+ Descriptor(DescriptorCommand),
/// Exit REPL loop.
Exit,
}
+++ /dev/null
-use std::collections::HashMap;
-
-use crate::config::WalletConfigInner;
-use crate::utils::output::{FormatOutput, simple_table};
-use crate::{error::BDKCliError as Error, utils::shorten};
-use bdk_wallet::bitcoin::{
- Address, Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex,
-};
-use bdk_wallet::{AddressInfo, Balance, LocalOutput, chain::ChainPosition};
-use cli_table::{Cell, CellStruct, Style, format::Justify};
-use serde::Serialize;
-use serde_json::json;
-
-/// Represent address result
-#[derive(Serialize)]
-pub struct AddressResult {
- pub address: String,
- pub index: u32,
-}
-
-impl From<AddressInfo> for AddressResult {
- fn from(info: AddressInfo) -> Self {
- Self {
- address: info.address.to_string(),
- index: info.index,
- }
- }
-}
-
-/// pretty presentation for address
-impl FormatOutput for AddressResult {
- fn to_table(&self) -> Result<String, Error> {
- simple_table(
- vec![
- vec!["Address".cell().bold(true), self.address.clone().cell()],
- vec![
- "Index".cell().bold(true),
- self.index.cell().justify(Justify::Right),
- ],
- ],
- None,
- )
- }
-}
-
-/// Represents the data for a single transaction
-#[derive(Serialize)]
-pub struct TransactionDetails {
- pub txid: String,
- pub is_coinbase: bool,
- pub wtxid: String,
- pub version: serde_json::Value,
- pub is_rbf: bool,
- pub inputs: serde_json::Value,
- pub outputs: serde_json::Value,
- #[serde(skip)]
- pub version_display: String,
- #[serde(skip)]
- pub input_count: usize,
- #[serde(skip)]
- pub output_count: usize,
- #[serde(skip)]
- pub total_value: u64,
-}
-
-/// A wrapper type for a list of transactions.
-#[derive(Serialize)]
-#[serde(transparent)]
-pub struct TransactionListResult(pub Vec<TransactionDetails>);
-
-impl FormatOutput for TransactionListResult {
- fn to_table(&self) -> Result<String, Error> {
- let rows = self.0.iter().map(|tx| {
- vec![
- tx.txid.clone().cell(),
- tx.version_display.clone().cell().justify(Justify::Right),
- tx.is_rbf.to_string().cell().justify(Justify::Center),
- tx.input_count.to_string().cell().justify(Justify::Right),
- tx.output_count.to_string().cell().justify(Justify::Right),
- tx.total_value.to_string().cell().justify(Justify::Right),
- ]
- });
-
- simple_table(
- rows,
- Some(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),
- ]),
- )
- }
-}
-
-/// Balance representation
-#[derive(Serialize)]
-pub struct BalanceResult {
- pub total: u64,
- pub trusted_pending: u64,
- pub untrusted_pending: u64,
- pub immature: u64,
- pub confirmed: u64,
-}
-
-impl From<Balance> for BalanceResult {
- fn from(b: Balance) -> Self {
- Self {
- total: b.total().to_sat(),
- confirmed: b.confirmed.to_sat(),
- trusted_pending: b.trusted_pending.to_sat(),
- untrusted_pending: b.untrusted_pending.to_sat(),
- immature: b.immature.to_sat(),
- }
- }
-}
-
-impl FormatOutput for BalanceResult {
- fn to_table(&self) -> Result<String, Error> {
- simple_table(
- vec![
- vec![
- "Total".cell().bold(true),
- self.total.cell().justify(Justify::Right),
- ],
- vec![
- "Confirmed".cell().bold(true),
- self.confirmed.cell().justify(Justify::Right),
- ],
- vec![
- "Trusted Pending".cell().bold(true),
- self.trusted_pending.cell().justify(Justify::Right),
- ],
- vec![
- "Untrusted Pending".cell().bold(true),
- self.untrusted_pending.cell().justify(Justify::Right),
- ],
- vec![
- "Immature".cell().bold(true),
- self.immature.cell().justify(Justify::Right),
- ],
- ],
- Some(vec![
- "Status".cell().bold(true),
- "Amount (sat)".cell().bold(true),
- ]),
- )
- }
-}
-
-/// single UTXO
-#[derive(Serialize)]
-pub struct UnspentDetails {
- pub outpoint: String,
- pub txout: serde_json::Value,
- pub keychain: String,
- pub is_spent: bool,
- pub derivation_index: u32,
- pub chain_position: serde_json::Value,
-
- #[serde(skip)]
- pub value_sat: u64,
- #[serde(skip)]
- pub address: String,
- #[serde(skip)]
- pub outpoint_display: String,
- #[serde(skip)]
- pub height_display: String,
- #[serde(skip)]
- pub block_hash_display: String,
-}
-
-impl UnspentDetails {
- pub fn from_local_output(utxo: &LocalOutput, network: Network) -> Self {
- let height = utxo.chain_position.confirmation_height_upper_bound();
- let height_display = height
- .map(|h| h.to_string())
- .unwrap_or_else(|| "Pending".to_string());
-
- let (_, block_hash_display) = match &utxo.chain_position {
- ChainPosition::Confirmed { anchor, .. } => {
- let hash = anchor.block_id.hash.to_string();
- (Some(hash.clone()), shorten(&hash, 8, 8))
- }
- ChainPosition::Unconfirmed { .. } => (None, "Unconfirmed".to_string()),
- };
-
- let address = Address::from_script(&utxo.txout.script_pubkey, network)
- .map(|a| a.to_string())
- .unwrap_or_else(|_| "Unknown Script".to_string());
-
- let outpoint_str = utxo.outpoint.to_string();
-
- Self {
- outpoint: outpoint_str.clone(),
- txout: serde_json::to_value(&utxo.txout).unwrap_or(json!({})),
- keychain: format!("{:?}", utxo.keychain),
- is_spent: utxo.is_spent,
- derivation_index: utxo.derivation_index,
- chain_position: serde_json::to_value(utxo.chain_position).unwrap_or(json!({})),
-
- value_sat: utxo.txout.value.to_sat(),
- address,
- outpoint_display: shorten(&outpoint_str, 8, 10),
- height_display,
- block_hash_display,
- }
- }
-}
-
-/// Wrapper for the list of UTXOs
-#[derive(Serialize)]
-#[serde(transparent)]
-pub struct UnspentListResult(pub Vec<UnspentDetails>);
-
-impl FormatOutput for UnspentListResult {
- fn to_table(&self) -> Result<String, Error> {
- let mut rows: Vec<Vec<CellStruct>> = vec![];
-
- for utxo in &self.0 {
- rows.push(vec![
- utxo.outpoint_display.clone().cell(),
- utxo.value_sat.to_string().cell().justify(Justify::Right),
- utxo.address.clone().cell(),
- utxo.keychain.clone().cell(),
- utxo.is_spent.cell(),
- utxo.derivation_index.cell(),
- utxo.height_display.clone().cell().justify(Justify::Right),
- utxo.block_hash_display
- .clone()
- .cell()
- .justify(Justify::Right),
- ]);
- }
-
- let 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),
- ];
- simple_table(rows, Some(title))
- }
-}
-
-#[derive(Serialize)]
-pub struct PsbtResult {
- pub psbt: String,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub is_finalized: Option<bool>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub details: Option<serde_json::Value>,
-}
-
-impl PsbtResult {
- pub fn new(psbt: &Psbt, verbose: bool, finalized: Option<bool>) -> Self {
- Self {
- psbt: bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD.encode(psbt.serialize()),
- is_finalized: finalized,
- details: if verbose {
- Some(serde_json::to_value(psbt).unwrap_or_default())
- } else {
- None
- },
- }
- }
-}
-
-impl FormatOutput for PsbtResult {
- fn to_table(&self) -> Result<String, Error> {
- let mut rows = vec![vec![
- "PSBT (Base64)".cell().bold(true),
- self.psbt.clone().cell(),
- ]];
-
- if let Some(finalized) = self.is_finalized {
- rows.push(vec!["Is Finalized".cell().bold(true), finalized.cell()]);
- }
-
- if self.details.is_some() {
- rows.push(vec![
- "Details".cell().bold(true),
- "Run without --pretty to view verbose JSON details".cell(),
- ]);
- }
-
- simple_table(rows, None)
- }
-}
-
-#[derive(Serialize)]
-pub struct RawPsbt {
- pub raw_tx: String,
-}
-
-impl RawPsbt {
- pub fn new(tx: &Transaction) -> Self {
- Self {
- raw_tx: serialize_hex(tx),
- }
- }
-}
-
-impl FormatOutput for RawPsbt {
- fn to_table(&self) -> Result<String, Error> {
- let rows = vec![vec![
- "Raw Transaction".cell().bold(true),
- self.raw_tx.clone().cell(),
- ]];
- simple_table(rows, None)
- }
-}
-
-#[derive(Serialize)]
-pub struct KeychainPair<T> {
- pub external: T,
- pub internal: T,
-}
-
-// Table formatting for string pairs (used by PublicDescriptor)
-impl FormatOutput for KeychainPair<String> {
- fn to_table(&self) -> Result<String, Error> {
- let rows = vec![
- vec!["External".cell().bold(true), self.external.clone().cell()],
- vec!["Internal".cell().bold(true), self.internal.clone().cell()],
- ];
- simple_table(rows, None)
- }
-}
-
-// Table formatting for JSON value pairs (used by Policies)
-impl FormatOutput for KeychainPair<serde_json::Value> {
- fn to_table(&self) -> Result<String, Error> {
- let ext_str = serde_json::to_string_pretty(&self.external)
- .map_err(|e| Error::Generic(e.to_string()))?;
- let int_str = serde_json::to_string_pretty(&self.internal)
- .map_err(|e| Error::Generic(e.to_string()))?;
-
- let rows = vec![
- vec!["External".cell().bold(true), ext_str.cell()],
- vec!["Internal".cell().bold(true), int_str.cell()],
- ];
- simple_table(rows, None)
- }
-}
-#[derive(Serialize)]
-pub struct KeyResult {
- pub xprv: String,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub xpub: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub mnemonic: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub fingerprint: Option<String>,
-}
-
-impl FormatOutput for KeyResult {
- fn to_table(&self) -> Result<String, Error> {
- let mut rows: Vec<Vec<CellStruct>> = vec![];
-
- if let Some(mnemonic) = &self.mnemonic {
- rows.push(vec!["Mnemonic".cell().bold(true), mnemonic.clone().cell()]);
- }
- if let Some(xpub) = &self.xpub {
- rows.push(vec!["Xpub".cell().bold(true), xpub.clone().cell()]);
- }
-
- rows.push(vec!["Xprv".cell().bold(true), self.xprv.clone().cell()]);
-
- if let Some(fingerprint) = &self.fingerprint {
- rows.push(vec![
- "Fingerprint".cell().bold(true),
- fingerprint.clone().cell(),
- ]);
- }
-
- simple_table(rows, None)
- }
-}
-
-#[derive(Serialize)]
-#[serde(transparent)]
-pub struct WalletsListResult(pub HashMap<String, WalletConfigInner>);
-
-impl FormatOutput for WalletsListResult {
- fn to_table(&self) -> Result<String, Error> {
- if self.0.is_empty() {
- return Ok("No wallets configured yet.".to_string());
- }
-
- let rows = self.0.iter().map(|(name, inner)| {
- let desc: String = inner.ext_descriptor.chars().take(30).collect();
- let desc_display = if inner.ext_descriptor.len() > 30 {
- format!("{}...", desc)
- } else {
- desc
- };
-
- vec![
- name.clone().cell(),
- inner.network.clone().cell(),
- desc_display.cell(),
- ]
- });
-
- simple_table(
- rows,
- Some(vec![
- "Wallet Name".cell().bold(true),
- "Network".cell().bold(true),
- "External Descriptor".cell().bold(true),
- ]),
- )
- }
-}
-
-#[derive(Serialize)]
-pub struct DescriptorResult {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub descriptor: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub multipath_descriptor: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub public_descriptors: Option<KeychainPair<String>>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub private_descriptors: Option<KeychainPair<String>>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub mnemonic: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub fingerprint: Option<String>,
-}
-
-impl FormatOutput for DescriptorResult {
- fn to_table(&self) -> Result<String, Error> {
- let mut rows: Vec<Vec<CellStruct>> = vec![];
-
- if let Some(desc) = &self.descriptor {
- rows.push(vec!["Descriptor".cell().bold(true), desc.clone().cell()]);
- }
- if let Some(desc) = &self.multipath_descriptor {
- rows.push(vec![
- "Multipath Descriptor".cell().bold(true),
- desc.clone().cell(),
- ]);
- }
- if let Some(pub_desc) = &self.public_descriptors {
- rows.push(vec![
- "External Public".cell().bold(true),
- pub_desc.external.clone().cell(),
- ]);
- rows.push(vec![
- "Internal Public".cell().bold(true),
- pub_desc.internal.clone().cell(),
- ]);
- }
- if let Some(priv_desc) = &self.private_descriptors {
- rows.push(vec![
- "External Private".cell().bold(true),
- priv_desc.external.clone().cell(),
- ]);
- rows.push(vec![
- "Internal Private".cell().bold(true),
- priv_desc.internal.clone().cell(),
- ]);
- }
- if let Some(mnemonic) = &self.mnemonic {
- rows.push(vec!["Mnemonic".cell().bold(true), mnemonic.clone().cell()]);
- }
-
- if let Some(fp) = &self.fingerprint {
- rows.push(vec!["Fingerprint".cell().bold(true), fp.clone().cell()]);
- }
-
- simple_table(rows, None)
- }
-}
-
-#[derive(Serialize, Debug, Default)]
-pub struct MessageResult {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub proof: Option<String>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub valid: Option<bool>,
-
- #[serde(skip_serializing_if = "Option::is_none")]
- pub proven_amount: Option<u64>,
-}
-
-impl FormatOutput for MessageResult {
- fn to_table(&self) -> Result<String, Error> {
- let mut rows = vec![];
-
- if let Some(proof) = &self.proof {
- rows.push(vec!["Proof".cell().bold(true), proof.cell()]);
- }
- if let Some(valid) = self.valid {
- rows.push(vec!["Is Valid".cell().bold(true), valid.to_string().cell()]);
- }
- if let Some(amount) = self.proven_amount {
- rows.push(vec![
- "Proven Amount (sats)".cell().bold(true),
- amount.cell(),
- ]);
- }
-
- let title = vec!["Property".cell().bold(true), "Value".cell().bold(true)];
-
- simple_table(rows, Some(title))
- }
-}
-
-#[derive(Serialize, Debug)]
-pub struct StatusResult {
- pub message: String,
-}
-
-impl StatusResult {
- pub fn new(msg: &str) -> Self {
- Self {
- message: msg.to_string(),
- }
- }
-}
-
-impl FormatOutput for StatusResult {
- fn to_table(&self) -> Result<String, Error> {
- Ok(self.message.clone())
- }
-}
-
-#[derive(Serialize, Debug)]
-pub struct TransactionResult {
- pub txid: String,
-}
-
-impl FormatOutput for TransactionResult {
- fn to_table(&self) -> Result<String, Error> {
- simple_table(
- vec![vec!["TXID".cell().bold(true), self.txid.clone().cell()]],
- None,
- )
- }
-}
-
-#[derive(Serialize, Debug)]
-pub struct ConfigResult {
- pub wallet_name: String,
- pub path: String,
- pub message: String,
-}
-
-impl FormatOutput for ConfigResult {
- fn to_table(&self) -> Result<String, Error> {
- Ok(format!("{} saved to {}", self.message, self.path))
- }
-}
--- /dev/null
+use std::collections::HashMap;
+
+use crate::config::WalletConfigInner;
+use crate::utils::output::FormatOutput;
+use crate::utils::shorten;
+use bdk_wallet::Balance;
+use bdk_wallet::bitcoin::{
+ Address, Network, Psbt, Transaction, base64::Engine, consensus::encode::serialize_hex,
+};
+use bdk_wallet::{AddressInfo, LocalOutput, chain::ChainPosition};
+use serde::Serialize;
+use serde_json::json;
+
+/// Represent address result
+#[derive(Serialize)]
+pub struct AddressResult {
+ pub address: String,
+ pub index: u32,
+}
+
+impl From<AddressInfo> for AddressResult {
+ fn from(info: AddressInfo) -> Self {
+ Self {
+ address: info.address.to_string(),
+ index: info.index,
+ }
+ }
+}
+
+impl FormatOutput for AddressResult {}
+
+/// Represents the data for a single transaction
+#[derive(Serialize)]
+pub struct TransactionDetails {
+ pub txid: String,
+ pub is_coinbase: bool,
+ pub wtxid: String,
+ pub version: serde_json::Value,
+ pub is_rbf: bool,
+ pub inputs: serde_json::Value,
+ pub outputs: serde_json::Value,
+ #[serde(skip)]
+ pub version_display: String,
+ #[serde(skip)]
+ pub input_count: usize,
+ #[serde(skip)]
+ pub output_count: usize,
+ #[serde(skip)]
+ pub total_value: u64,
+}
+
+/// A wrapper type for a list of transactions.
+#[derive(Serialize)]
+#[serde(transparent)]
+pub struct TransactionListResult(pub Vec<TransactionDetails>);
+
+impl FormatOutput for TransactionListResult {}
+
+/// single UTXO
+#[derive(Serialize)]
+pub struct UnspentDetails {
+ pub outpoint: String,
+ pub txout: serde_json::Value,
+ pub keychain: String,
+ pub is_spent: bool,
+ pub derivation_index: u32,
+ pub chain_position: serde_json::Value,
+
+}
+
+impl UnspentDetails {
+ pub fn from_local_output(utxo: &LocalOutput, network: Network) -> Self {
+ let height = utxo.chain_position.confirmation_height_upper_bound();
+ let height_display = height
+ .map(|h| h.to_string())
+ .unwrap_or_else(|| "Pending".to_string());
+
+ let (_, block_hash_display) = match &utxo.chain_position {
+ ChainPosition::Confirmed { anchor, .. } => {
+ let hash = anchor.block_id.hash.to_string();
+ (Some(hash.clone()), shorten(&hash, 8, 8))
+ }
+ ChainPosition::Unconfirmed { .. } => (None, "Unconfirmed".to_string()),
+ };
+
+ let address = Address::from_script(&utxo.txout.script_pubkey, network)
+ .map(|a| a.to_string())
+ .unwrap_or_else(|_| "Unknown Script".to_string());
+
+ let outpoint_str = utxo.outpoint.to_string();
+
+ Self {
+ outpoint: outpoint_str.clone(),
+ txout: serde_json::to_value(&utxo.txout).unwrap_or(json!({})),
+ keychain: format!("{:?}", utxo.keychain),
+ is_spent: utxo.is_spent,
+ derivation_index: utxo.derivation_index,
+ chain_position: serde_json::to_value(utxo.chain_position).unwrap_or(json!({})),
+ }
+ }
+}
+
+/// Wrapper for the list of UTXOs
+#[derive(Serialize)]
+#[serde(transparent)]
+pub struct UnspentListResult(pub Vec<UnspentDetails>);
+
+impl FormatOutput for UnspentListResult {}
+
+#[derive(Serialize)]
+pub struct PsbtResult {
+ pub psbt: String,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub is_finalized: Option<bool>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub details: Option<serde_json::Value>,
+}
+
+impl PsbtResult {
+ pub fn new(psbt: &Psbt, verbose: bool, finalized: Option<bool>) -> Self {
+ Self {
+ psbt: bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD.encode(psbt.serialize()),
+ is_finalized: finalized,
+ details: if verbose {
+ Some(serde_json::to_value(psbt).unwrap_or_default())
+ } else {
+ None
+ },
+ }
+ }
+}
+
+impl FormatOutput for PsbtResult {}
+
+#[derive(Serialize)]
+pub struct RawPsbt {
+ pub raw_tx: String,
+}
+
+impl RawPsbt {
+ pub fn new(tx: &Transaction) -> Self {
+ Self {
+ raw_tx: serialize_hex(tx),
+ }
+ }
+}
+
+impl FormatOutput for RawPsbt {}
+
+#[derive(Serialize)]
+pub struct KeychainPair<T> {
+ pub external: T,
+ pub internal: T,
+}
+
+impl FormatOutput for KeychainPair<String> {}
+
+// Table formatting for JSON value pairs (used by Policies)
+impl FormatOutput for KeychainPair<serde_json::Value> {}
+
+#[derive(Serialize, Debug, Default)]
+pub struct MessageResult {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub proof: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub valid: Option<bool>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub proven_amount: Option<u64>,
+}
+
+impl FormatOutput for MessageResult {}
+
+#[derive(Serialize, Debug)]
+pub struct StatusResult {
+ pub message: String,
+}
+
+impl StatusResult {
+ pub fn new(msg: &str) -> Self {
+ Self {
+ message: msg.to_string(),
+ }
+ }
+}
+
+impl FormatOutput for StatusResult {}
+
+#[derive(Serialize, Debug)]
+pub struct TransactionResult {
+ pub txid: String,
+}
+
+impl FormatOutput for TransactionResult {}
+
+
+
+/// Return type definition
+#[derive(Serialize)]
+#[serde(transparent)]
+pub struct WalletsListResult(pub HashMap<String, WalletConfigInner>);
+
+impl FormatOutput for WalletsListResult {}
+
+
+/// return type
+#[derive(Serialize)]
+pub struct DescriptorResult {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub descriptor: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub multipath_descriptor: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub public_descriptors: Option<KeychainPair<String>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub private_descriptors: Option<KeychainPair<String>>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mnemonic: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub fingerprint: Option<String>,
+}
+
+impl FormatOutput for DescriptorResult {}
+
+#[derive(Serialize)]
+pub struct KeyResult {
+ pub xprv: String,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub xpub: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mnemonic: Option<String>,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub fingerprint: Option<String>,
+}
+
+impl FormatOutput for KeyResult {}
+
+
+/// Balance representation
+#[derive(Serialize)]
+pub struct BalanceResult {
+ pub total: u64,
+ pub trusted_pending: u64,
+ pub untrusted_pending: u64,
+ pub immature: u64,
+ pub confirmed: u64,
+}
+
+impl From<Balance> for BalanceResult {
+ fn from(b: Balance) -> Self {
+ Self {
+ total: b.total().to_sat(),
+ confirmed: b.confirmed.to_sat(),
+ trusted_pending: b.trusted_pending.to_sat(),
+ untrusted_pending: b.untrusted_pending.to_sat(),
+ immature: b.immature.to_sat(),
+ }
+ }
+}
+
+impl FormatOutput for BalanceResult {}