diff --git a/CHANGELOG.md b/CHANGELOG.md index 098eee19..c9ed1976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. ## [Unreleased] - Added support for Multipath (two-paths) descriptors. - +- Support for JSON and TOML format through `--format` flag. ## [4.0.0] diff --git a/src/commands.rs b/src/commands.rs index 37f80523..aa24cf7b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -86,11 +86,23 @@ pub struct CliOpts { /// Default value : ~/.bdk-bitcoin #[arg(env = "DATADIR", short = 'd', long = "datadir")] pub datadir: Option, + /// Sets the output format. + #[arg(env = "FORMAT", short = 'f', long = "format", default_value = "json")] + pub format: OutputFormatType, /// Top level cli sub-commands. #[command(subcommand)] pub subcommand: CliSubCommand, } +/// Supported output formats. +#[derive(Clone, Debug, PartialEq, Eq, clap::ValueEnum)] +pub enum OutputFormatType { + /// JSON output. + Json, + /// TOML output. + Toml, +} + /// Top level cli sub-commands. #[derive(Debug, Subcommand, Clone, PartialEq)] #[command(rename_all = "snake")] diff --git a/src/handlers/key.rs b/src/handlers/key.rs index e7bb6457..de1ef649 100644 --- a/src/handlers/key.rs +++ b/src/handlers/key.rs @@ -1,4 +1,4 @@ -use crate::commands::KeySubCommand; +use crate::commands::{KeySubCommand, OutputFormatType}; use crate::error::BDKCliError as Error; use crate::handlers::{AppCommand, AppContext, Init}; use crate::utils::{output::FormatOutput, types::KeyResult}; @@ -12,17 +12,21 @@ use bdk_wallet::miniscript::{self, Segwitv0}; use clap::Parser; impl KeySubCommand { - pub fn execute(&self, ctx: &mut AppContext) -> Result<(), Error> { + pub fn execute( + &self, + ctx: &mut AppContext, + format: OutputFormatType, + ) -> Result<(), Error> { match self { KeySubCommand::Generate(generate_key_command) => generate_key_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), KeySubCommand::Restore(restore_key_command) => restore_key_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), KeySubCommand::Derive(derive_key_command) => derive_key_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), } } } diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index fe4097e4..c36fc913 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -1,4 +1,4 @@ -use crate::commands::OfflineWalletSubCommand; +use crate::commands::{OfflineWalletSubCommand, OutputFormatType}; use crate::error::BDKCliError as Error; use crate::handlers::{AppCommand, AppContext, OfflineOperations}; use crate::utils::output::{FormatOutput, ListResult}; @@ -36,56 +36,68 @@ use { }; impl OfflineWalletSubCommand { - pub fn execute(&self, ctx: &mut AppContext>) -> Result<(), Error> { + pub fn execute( + &self, + ctx: &mut AppContext>, + format: OutputFormatType, + ) -> Result<(), Error> { match self { - Self::NewAddress(new_address) => new_address.execute(ctx)?.write_out(std::io::stdout()), - Self::Balance(balance) => balance.execute(ctx)?.write_out(std::io::stdout()), + Self::NewAddress(new_address) => new_address + .execute(ctx)? + .write_out(std::io::stdout(), format), + Self::Balance(balance) => balance.execute(ctx)?.write_out(std::io::stdout(), format), Self::UnusedAddress(unused_address_command) => unused_address_command .execute(ctx)? - .write_out(std::io::stdout()), - Self::Unspent(unspent_command) => { - unspent_command.execute(ctx)?.write_out(std::io::stdout()) - } + .write_out(std::io::stdout(), format), + Self::Unspent(unspent_command) => unspent_command + .execute(ctx)? + .write_out(std::io::stdout(), format), Self::Transactions(transactions_command) => transactions_command .execute(ctx)? - .write_out(std::io::stdout()), - Self::CreateTx(createtx_command) => { - createtx_command.execute(ctx)?.write_out(std::io::stdout()) - } + .write_out(std::io::stdout(), format), + Self::CreateTx(createtx_command) => createtx_command + .execute(ctx)? + .write_out(std::io::stdout(), format), #[cfg(feature = "silent-payments")] - Self::CreateSpTx(cmd) => cmd.execute(ctx)?.write_out(std::io::stdout()), - Self::BumpFee(bumpfee_command) => { - bumpfee_command.execute(ctx)?.write_out(std::io::stdout()) - } - Self::Policies(policies_command) => { - policies_command.execute(ctx)?.write_out(std::io::stdout()) - } + Self::CreateSpTx(cmd) => cmd.execute(ctx)?.write_out(std::io::stdout(), format), + Self::BumpFee(bumpfee_command) => bumpfee_command + .execute(ctx)? + .write_out(std::io::stdout(), format), + Self::Policies(policies_command) => policies_command + .execute(ctx)? + .write_out(std::io::stdout(), format), Self::PublicDescriptor(public_descriptor_command) => public_descriptor_command .execute(ctx)? - .write_out(std::io::stdout()), - Self::Sign(sign_command) => sign_command.execute(ctx)?.write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), + Self::Sign(sign_command) => sign_command + .execute(ctx)? + .write_out(std::io::stdout(), format), Self::ExtractPsbt(extract_psbt_command) => extract_psbt_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), Self::FinalizePsbt(finalize_psbt_command) => finalize_psbt_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), Self::CombinePsbt(combine_psbt_command) => combine_psbt_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), #[cfg(feature = "message_signer")] Self::SignMessage(sign_message_command) => sign_message_command .execute(ctx)? - .write_out(std::io::stdout()), + .write_out(std::io::stdout(), format), #[cfg(feature = "message_signer")] Self::VerifyMessage(verify_message_command) => verify_message_command .execute(ctx)? - .write_out(std::io::stdout()), - Self::LockUtxo(lock_utxo) => lock_utxo.execute(ctx)?.write_out(std::io::stdout()), - Self::UnlockUtxo(unlock_utxo) => unlock_utxo.execute(ctx)?.write_out(std::io::stdout()), - Self::LockedUtxos(locked_utxos) => { - locked_utxos.execute(ctx)?.write_out(std::io::stdout()) + .write_out(std::io::stdout(), format), + Self::LockUtxo(lock_utxo) => { + lock_utxo.execute(ctx)?.write_out(std::io::stdout(), format) } + Self::UnlockUtxo(unlock_utxo) => unlock_utxo + .execute(ctx)? + .write_out(std::io::stdout(), format), + Self::LockedUtxos(locked_utxos) => locked_utxos + .execute(ctx)? + .write_out(std::io::stdout(), format), #[cfg(feature = "dns_payment")] Self::CreateDnsTx(_) => Err(Error::Generic( "CreateDnsTx is dispatched asynchronously through main".to_string(), diff --git a/src/handlers/online.rs b/src/handlers/online.rs index f2900cbc..2c337f2f 100644 --- a/src/handlers/online.rs +++ b/src/handlers/online.rs @@ -4,6 +4,13 @@ use clap::Parser; use crate::client::BlockchainClient::Electrum; #[cfg(feature = "cbf")] use crate::client::{BlockchainClient::KyotoClient, sync_kyoto_client}; +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +use crate::commands::OutputFormatType; #[cfg(feature = "esplora")] use {crate::client::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt}; #[cfg(feature = "rpc")] @@ -44,35 +51,39 @@ use { feature = "rpc" ))] impl OnlineWalletSubCommand { - pub async fn execute(&self, ctx: &mut AppContext>) -> Result<(), Error> { + pub async fn execute( + &self, + ctx: &mut AppContext>, + format: OutputFormatType, + ) -> Result<(), Error> { match self { OnlineWalletSubCommand::FullScan(full_scan_command) => { let response: StatusResult = full_scan_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } OnlineWalletSubCommand::Sync(sync_command) => { let response: StatusResult = sync_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } OnlineWalletSubCommand::Broadcast(broadcast_command) => { let response: TransactionResult = broadcast_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } OnlineWalletSubCommand::ReceivePayjoin(receive_payjoin_command) => { let response: StatusResult = receive_payjoin_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } OnlineWalletSubCommand::SendPayjoin(send_payjoin_command) => { let response: StatusResult = send_payjoin_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } OnlineWalletSubCommand::ResumePayjoin(resume_payjoin_command) => { let response: StatusResult = resume_payjoin_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } OnlineWalletSubCommand::PayjoinHistory(payjoin_history_command) => { let response: StatusResult = payjoin_history_command.execute(ctx).await?; - response.write_out(std::io::stdout()) + response.write_out(std::io::stdout(), format) } } } diff --git a/src/handlers/repl.rs b/src/handlers/repl.rs index b15b653f..0559e16c 100644 --- a/src/handlers/repl.rs +++ b/src/handlers/repl.rs @@ -18,6 +18,8 @@ use { ))] use crate::client::BlockchainClient; #[cfg(feature = "repl")] +use crate::commands::OutputFormatType; +#[cfg(feature = "repl")] use {crate::commands::WalletSubCommand, crate::error::BDKCliError as Error, std::io::Write}; #[cfg(feature = "repl")] @@ -40,6 +42,7 @@ pub(crate) async fn respond( feature = "cbf" ))] wallet_name: &str, + format: OutputFormatType, ) -> Result { let args = shlex::split(line).ok_or("error: Invalid quoting".to_string())?; @@ -55,7 +58,7 @@ pub(crate) async fn respond( ReplSubCommand::Wallet { subcommand } => match subcommand { WalletSubCommand::OfflineWalletSubCommand(cmd) => { let mut ctx = AppContext::new_offline_wallet(network, datadir, wallet); - cmd.execute(&mut ctx).map_err(|e| e.to_string())?; + cmd.execute(&mut ctx, format).map_err(|e| e.to_string())?; Some(()) } #[cfg(any( @@ -74,7 +77,9 @@ pub(crate) async fn respond( wallet_name.to_string(), ); - cmd.execute(&mut ctx).await.map_err(|e| e.to_string())?; + cmd.execute(&mut ctx, format) + .await + .map_err(|e| e.to_string())?; Some(()) } WalletSubCommand::Config(_) => { @@ -92,14 +97,16 @@ pub(crate) async fn respond( let mut ctx = AppContext::new(network, datadir); cmd.execute(&mut ctx) .map_err(|e| e.to_string())? - .write_out(std::io::stdout()) + .write_out(std::io::stdout(), format) .map_err(|e| e.to_string())?; Some(()) } ReplSubCommand::Key { subcommand } => { let mut ctx = AppContext::new(network, datadir); - subcommand.execute(&mut ctx).map_err(|e| e.to_string())?; + subcommand + .execute(&mut ctx, format) + .map_err(|e| e.to_string())?; Some(()) } diff --git a/src/main.rs b/src/main.rs index 06e3ea24..27a28be8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,7 +78,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { runtime.wallet_name.clone(), ); - cmd.execute(&mut ctx).await?; + cmd.execute(&mut ctx, cli_opts.format).await?; } wallet.persist()?; } @@ -100,9 +100,9 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { dns_cmd .execute(&mut ctx) .await? - .write_out(std::io::stdout())?; + .write_out(std::io::stdout(), cli_opts.format)?; } - other => other.execute(&mut ctx)?, + other => other.execute(&mut ctx, cli_opts.format)?, } } wallet.persist()?; @@ -113,26 +113,30 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { let mut ctx = AppContext::new(cli_opts.network, home_dir); - config_cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; + config_cmd + .execute(&mut ctx)? + .write_out(std::io::stdout(), cli_opts.format)?; } }, CliSubCommand::Key { subcommand } => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - subcommand.execute(&mut ctx)?; + subcommand.execute(&mut ctx, cli_opts.format)?; } CliSubCommand::Descriptor(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; + cmd.execute(&mut ctx)? + .write_out(std::io::stdout(), cli_opts.format)?; } CliSubCommand::Wallets(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; + cmd.execute(&mut ctx)? + .write_out(std::io::stdout(), cli_opts.format)?; } #[cfg(feature = "repl")] @@ -183,6 +187,7 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { feature = "cbf" ))] &wallet_name, + cli_opts.format.clone(), ) .await .map_err(Error::Generic)?; @@ -197,7 +202,8 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { CliSubCommand::Compile(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; + cmd.execute(&mut ctx)? + .write_out(std::io::stdout(), cli_opts.format)?; } CliSubCommand::Completions { shell } => { clap_complete::generate( @@ -211,12 +217,15 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { CliSubCommand::SilentPaymentCode(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; + cmd.execute(&mut ctx)? + .write_out(std::io::stdout(), cli_opts.format)?; } #[cfg(feature = "dns_payment")] CliSubCommand::ResolveDnsRecipient(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); - cmd.execute(&mut ctx).await?.write_out(std::io::stdout())?; + cmd.execute(&mut ctx) + .await? + .write_out(std::io::stdout(), cli_opts.format)?; } } diff --git a/src/utils/output.rs b/src/utils/output.rs index b2cd32a2..071c4381 100644 --- a/src/utils/output.rs +++ b/src/utils/output.rs @@ -1,18 +1,23 @@ use std::io::Write; -use crate::error::BDKCliError as Error; +use crate::{commands::OutputFormatType, error::BDKCliError as Error}; use serde::Serialize; /// A trait for types that can be presented to the user. pub trait FormatOutput: Serialize { - fn format(&self) -> Result { - serde_json::to_string_pretty(self) - .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}"))) + /// Formats the output according to the requested [`OutputFormatType`]. + fn format(&self, format: OutputFormatType) -> Result { + match format { + OutputFormatType::Json => serde_json::to_string_pretty(self) + .map_err(|e| Error::Generic(format!("JSON serialization failed: {e}"))), + OutputFormatType::Toml => toml::to_string_pretty(self) + .map_err(|e| Error::Generic(format!("TOML serialization failed: {e}"))), + } } - fn write_out(&self, mut writer: W) -> Result<(), Error> { - let output = self.format()?; + fn write_out(&self, mut writer: W, format: OutputFormatType) -> Result<(), Error> { + let output = self.format(format)?; writeln!(writer, "{}", output) .map_err(|e| Error::Generic(format!("Failed to write output: {e}"))) }