From: Vihiga Tyonum Date: Thu, 21 May 2026 15:17:39 +0000 (+0100) Subject: ref(pretty): remove `--pretty` flag X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/struct.ScriptHash.html?a=commitdiff_plain;h=528956d244a1eb83b255c8e6a37fef66fa3967ea;p=bdk-cli ref(pretty): remove `--pretty` flag - remove --pretty flag - move types to utils - update commands --- diff --git a/README.md b/README.md index 2073a67..3522999 100644 --- a/README.md +++ b/README.md @@ -254,15 +254,6 @@ Note: You can modify the `Justfile` to reflect your nodes' configuration values. 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: diff --git a/src/commands.rs b/src/commands.rs index 127d32b..cb1c925 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,34 +13,38 @@ //! 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; @@ -72,9 +76,6 @@ pub struct CliOpts { /// Default value : ~/.bdk-bitcoin #[arg(env = "DATADIR", short = 'd', long = "datadir")] pub datadir: Option, - /// 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, @@ -110,17 +111,9 @@ pub enum 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. /// @@ -131,24 +124,15 @@ pub enum CliSubCommand { #[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, - }, + 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 @@ -205,114 +189,23 @@ pub enum CliSubCommand { }, } -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 { - 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 { @@ -328,27 +221,27 @@ pub struct WalletOpts { /// Sets the descriptor to use for internal/change addresses. #[arg(env = "INT_DESCRIPTOR", short = 'i', long)] pub int_descriptor: Option, - // #[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', @@ -356,7 +249,7 @@ pub struct WalletOpts { default_value = "5" )] pub parallel_requests: usize, - // #[cfg(feature = "rpc")] + #[cfg(feature = "rpc")] /// Sets the rpc basic authentication. #[arg( env = "USER:PASSWD", @@ -366,11 +259,11 @@ pub struct WalletOpts { 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, - // #[cfg(feature = "cbf")] + #[cfg(feature = "cbf")] #[clap(flatten)] pub compactfilter_opts: CompactFilterOpts, } @@ -426,126 +319,23 @@ pub enum OfflineWalletSubCommand { 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>, - // /// Marks a utxo as unspendable. - // #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] - // unspendable: Option>, - // /// Fee rate to use in sat/vbyte. - // #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")] - // fee_rate: Option, - // /// Selects which policy should be used to satisfy the external descriptor. - // #[arg(env = "EXT_POLICY", long = "external_policy")] - // external_policy: Option, - // /// Selects which policy should be used to satisfy the internal descriptor. - // #[arg(env = "INT_POLICY", long = "internal_policy")] - // internal_policy: Option, - // /// 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, - // /// 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, //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
, - // /// 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>, - // /// 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>, - // /// 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, - // /// 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, - // }, + /// 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, - // /// 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, - // }, 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, - // }, CombinePsbt(CombinePsbtCommand), /// Sign a message using BIP322 - // #[cfg(feature = "bip322")] + #[cfg(feature = "bip322")] // SignMessage { // /// The message to sign // #[arg(long)] @@ -562,7 +352,7 @@ pub enum OfflineWalletSubCommand { // }, SignMessage(SignMessageCommand), /// Verify a BIP322 signature - // #[cfg(feature = "bip322")] + #[cfg(feature = "bip322")] // VerifyMessage { // /// The signature proof to verify // #[arg(long)] @@ -588,33 +378,10 @@ pub enum OfflineWalletSubCommand { // ))] 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, - // /// Sets the raw transaction to broadcast. - // #[arg( - // env = "RAWTX", - // long = "tx", - // required_unless_present = "psbt", - // conflicts_with = "psbt" - // )] - // tx: Option, - // }, Broadcast(BroadcastCommand), /// Generates a Payjoin receive URI and processes the sender's Payjoin proposal. // ReceivePayjoin { @@ -659,37 +426,11 @@ pub enum OnlineWalletSubCommand { #[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, - }, + 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, - }, + 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. @@ -708,18 +449,7 @@ pub enum ReplSubCommand { 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, - }, + Descriptor(DescriptorCommand), /// Exit REPL loop. Exit, } diff --git a/src/handlers/types.rs b/src/handlers/types.rs deleted file mode 100644 index f6f03d4..0000000 --- a/src/handlers/types.rs +++ /dev/null @@ -1,573 +0,0 @@ -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 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 { - 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); - -impl FormatOutput for TransactionListResult { - fn to_table(&self) -> Result { - 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 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 { - 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); - -impl FormatOutput for UnspentListResult { - fn to_table(&self) -> Result { - let mut rows: Vec> = 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, - - #[serde(skip_serializing_if = "Option::is_none")] - pub details: Option, -} - -impl PsbtResult { - pub fn new(psbt: &Psbt, verbose: bool, finalized: Option) -> 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 { - 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 { - let rows = vec![vec![ - "Raw Transaction".cell().bold(true), - self.raw_tx.clone().cell(), - ]]; - simple_table(rows, None) - } -} - -#[derive(Serialize)] -pub struct KeychainPair { - pub external: T, - pub internal: T, -} - -// Table formatting for string pairs (used by PublicDescriptor) -impl FormatOutput for KeychainPair { - fn to_table(&self) -> Result { - 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 { - fn to_table(&self) -> Result { - 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, - - #[serde(skip_serializing_if = "Option::is_none")] - pub mnemonic: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub fingerprint: Option, -} - -impl FormatOutput for KeyResult { - fn to_table(&self) -> Result { - let mut rows: Vec> = 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); - -impl FormatOutput for WalletsListResult { - fn to_table(&self) -> Result { - 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, - - #[serde(skip_serializing_if = "Option::is_none")] - pub multipath_descriptor: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub public_descriptors: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub private_descriptors: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub mnemonic: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub fingerprint: Option, -} - -impl FormatOutput for DescriptorResult { - fn to_table(&self) -> Result { - let mut rows: Vec> = 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, - - #[serde(skip_serializing_if = "Option::is_none")] - pub valid: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub proven_amount: Option, -} - -impl FormatOutput for MessageResult { - fn to_table(&self) -> Result { - 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 { - Ok(self.message.clone()) - } -} - -#[derive(Serialize, Debug)] -pub struct TransactionResult { - pub txid: String, -} - -impl FormatOutput for TransactionResult { - fn to_table(&self) -> Result { - 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 { - Ok(format!("{} saved to {}", self.message, self.path)) - } -} diff --git a/src/utils/types.rs b/src/utils/types.rs new file mode 100644 index 0000000..f3c5ddc --- /dev/null +++ b/src/utils/types.rs @@ -0,0 +1,272 @@ +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 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); + +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); + +impl FormatOutput for UnspentListResult {} + +#[derive(Serialize)] +pub struct PsbtResult { + pub psbt: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub is_finalized: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl PsbtResult { + pub fn new(psbt: &Psbt, verbose: bool, finalized: Option) -> 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 { + pub external: T, + pub internal: T, +} + +impl FormatOutput for KeychainPair {} + +// Table formatting for JSON value pairs (used by Policies) +impl FormatOutput for KeychainPair {} + +#[derive(Serialize, Debug, Default)] +pub struct MessageResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub valid: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub proven_amount: Option, +} + +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); + +impl FormatOutput for WalletsListResult {} + + +/// return type +#[derive(Serialize)] +pub struct DescriptorResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub descriptor: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub multipath_descriptor: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub public_descriptors: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub private_descriptors: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub mnemonic: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub fingerprint: Option, +} + +impl FormatOutput for DescriptorResult {} + +#[derive(Serialize)] +pub struct KeyResult { + pub xprv: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub xpub: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub mnemonic: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub fingerprint: Option, +} + +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 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 {}