page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
## [Unreleased]
-- Add wallet configs initialization for initialiazing and saving wallet configs
- Add wallet subcommand `config` to save wallet configs
+- Add `wallets` command to list all wallets saved configs
## [2.0.0]
[[package]]
name = "bdk_wallet"
-version = "2.3.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b03f1e31ccc562f600981f747d2262b84428cbff52c9c9cdf14d15fb15bd2286"
+checksum = "d30b5dba770184863b5d966ccbc6a11d12c145450be3b6a4435308297e6a12dc"
dependencies = [
"bdk_chain",
"bip39",
```
This is available for wallet, key, repl and compile features. When ommitted, outputs default to `JSON`.
-## Initializing Wallet Configurations with `init` Subcommand
+## Saving and using wallet configurations
-The `wallet init` sub-command simplifies wallet operations by saving configuration parameters to `config.toml` in the data directory (default `~/.bdk-bitcoin/config.toml`). This allows you to run subsequent `bdk-cli wallet` commands without repeatedly specifying configuration details, easing wallet operations.
+The `wallet config` sub-command allows you to save wallet settings to a `config.toml` file in the default directory (`~/.bdk-bitcoin/`) or custom directory specified with the `--datadir` flag. This eliminate the need to repeatedly specify descriptors, client types, and other parameters for each command. Once configured, you can use any wallet command by simply specifying the wallet name. All other parameters are automatically loaded from the saved configuration.
-To initialize a wallet configuration, use the following command structure:
+To save a wallet settings:
```shell
-cargo run --features <list-of-features> -- -n <network> wallet --wallet <wallet_name> --ext-descriptor <ext_descriptor> --int-descriptor <int_descriptor> --client-type <client_type> --url <server_url> [--database-type <database_type>] [--rpc-user <rpc_user>]
- [--rpc-password <rpc_password>] init
+cargo run --features <list-of-features> -- -n <network> wallet --wallet <wallet_name> config [ -f ] --ext-descriptor <ext_descriptor> --int-descriptor <int_descriptor> --client-type <client_type> --url <server_url> [--database-type <database_type>] [--rpc-user <rpc_user>]
+ [--rpc-password <rpc_password>]
```
For example, to initialize a wallet named `my_wallet` with `electrum` as the backend on `signet` network:
```shell
-cargo run --features electrum -- -n signet wallet -w my_wallet -e "tr(tprv8Z.../0/*)#dtdqk3dx" -i "tr(tprv8Z.../1/*)#ulgptya7" -d sqlite -c electrum -u "ssl://mempool.space:60602" init
+cargo run --features electrum -- -n signet wallet -w my_wallet config -e "tr(tprv8Z.../0/*)#dtdqk3dx" -i "tr(tprv8Z.../1/*)#ulgptya7" -d sqlite -c electrum -u "ssl://mempool.space:60602"
```
-To overwrite an existing wallet configuration, use the `--force` flag after the `init` sub-command.
+To overwrite an existing wallet configuration, use the `--force` flag after the `config` sub-command.
-You can omit the following arguments to use their default values:
+#### Using a Configured Wallet
-`network`: Defaults to `testnet`
+Once configured, use any wallet command with just the wallet name:
-`database_type`: Defaults to `sqlite`
-
-#### Using Saved Configuration
-
-After a wallet is initialized, you can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet subcommand.
-
-For example, with the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow:
```shell
-cargo run wallet -w my_wallet --use-config new_address
+cargo run --features electrum wallet -w my_wallet new_address
-cargo run --features electrum wallet -w my_wallet --use-config sync
+cargo run --features electrum wallet -w my_wallet full_scan
```
Note that each wallet has its own configuration, allowing multiple wallets with different configurations.
+
+#### View all saved Wallet Configs
+
+To view all saved wallet configurations:
+
+```shell
+cargo run wallets`
+```
+You can also use the `--pretty` flag for a formatted output.
/// Wallet name for this REPL session
#[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)]
wallet: String,
-
- #[command(flatten)]
- wallet_opts: WalletOpts,
},
/// Output Descriptors operations.
///
/// Optional key: xprv, xpub, or mnemonic phrase
key: Option<String>,
},
+ /// List all saved wallet configurations.
+ Wallets,
}
/// Wallet operation subcommands.
#[derive(Debug, Subcommand, Clone, PartialEq)]
/// Selects the wallet to use.
#[arg(skip)]
pub wallet: Option<String>,
- // #[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)]
/// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects.
#[arg(env = "VERBOSE", short = 'v', long = "verbose")]
pub verbose: bool,
"testnet" => Network::Testnet,
"regtest" => Network::Regtest,
"signet" => Network::Signet,
+ "testnet4" => Network::Testnet4,
_ => {
return Err(Error::Generic("Invalid network".to_string()));
}
return Err(Error::Generic("Invalid database type".to_string()));
}
};
-
#[cfg(any(
feature = "electrum",
feature = "esplora",
.clone()
.ok_or_else(|| Error::Generic(format!("Server url not found")))?,
#[cfg(feature = "electrum")]
- batch_size: 10,
+ batch_size: wallet_config.batch_size.unwrap_or(10),
#[cfg(feature = "esplora")]
- parallel_requests: 5,
+ parallel_requests: wallet_config.parallel_requests.unwrap_or(5),
#[cfg(feature = "rpc")]
basic_auth: (
wallet_config
#[cfg(feature = "rpc")]
cookie: wallet_config.cookie.clone(),
#[cfg(feature = "cbf")]
- compactfilter_opts: crate::commands::CompactFilterOpts {
- conn_count: 2,
- skip_blocks: None,
- },
+ compactfilter_opts: crate::commands::CompactFilterOpts { conn_count: 2 },
})
}
}
feature = "rpc"
))]
use std::sync::Arc;
+
#[cfg(any(
feature = "electrum",
feature = "esplora",
wallet_opts: &WalletOpts,
force: bool,
) -> Result<String, Error> {
+ if network == Network::Bitcoin {
+ eprintln!(
+ "WARNING: You are configuring a wallet for Bitcoin MAINNET.
+ This software is experimental and not recommended for use with real funds.
+ Consider using a testnet for testing purposes. \n"
+ );
+ }
+
+ let ext_descriptor = wallet_opts.ext_descriptor.clone();
+ let int_descriptor = wallet_opts.int_descriptor.clone();
+
+ if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") {
+ eprintln!(
+ "WARNING: Your external descriptor contains PRIVATE KEYS.
+ Private keys will be saved in PLAINTEXT in the config file.
+ This is a security risk. Consider using public descriptors instead.\n"
+ );
+ }
+
+ if let Some(ref internal_desc) = int_descriptor {
+ if internal_desc.contains("xprv") || internal_desc.contains("tprv") {
+ eprintln!(
+ "WARNING: Your internal descriptor contains PRIVATE KEYS.
+ Private keys will be saved in PLAINTEXT in the config file.
+ This is a security risk. Consider using public descriptors instead.\n"
+ );
+ }
+ }
+
let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig {
network,
wallets: HashMap::new(),
)));
}
- let ext_descriptor = wallet_opts.ext_descriptor.clone();
- let int_descriptor = wallet_opts.int_descriptor.clone();
#[cfg(any(
feature = "electrum",
feature = "esplora",
}
}
+/// Handle wallets command to show all saved wallet configurations
+pub fn handle_wallets_subcommand(datadir: &Path, pretty: bool) -> Result<String, Error> {
+ let load_config = WalletConfig::load(datadir)?;
+
+ let config = match load_config {
+ Some(c) if !c.wallets.is_empty() => c,
+ _ => {
+ return Ok(if pretty {
+ "No wallet configurations found.".to_string()
+ } else {
+ serde_json::to_string_pretty(&json!({
+ "wallets": []
+ }))?
+ });
+ }
+ };
+
+ if pretty {
+ let mut rows: Vec<Vec<CellStruct>> = vec![];
+
+ for (name, wallet_config) in config.wallets.iter() {
+ let mut row = vec![name.cell(), wallet_config.network.clone().cell()];
+
+ #[cfg(any(feature = "sqlite", feature = "redb"))]
+ row.push(wallet_config.database_type.clone().cell());
+
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+ ))]
+ {
+ let client_str = wallet_config.client_type.as_deref().unwrap_or("N/A");
+ row.push(client_str.cell());
+ }
+
+ #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+ {
+ let url_str = wallet_config.server_url.as_deref().unwrap_or("N/A");
+ let display_url = if url_str.len() > 20 {
+ shorten(url_str, 15, 10)
+ } else {
+ url_str.to_string()
+ };
+ row.push(display_url.cell());
+ }
+
+ let ext_desc_display = if wallet_config.ext_descriptor.len() > 40 {
+ shorten(&wallet_config.ext_descriptor, 20, 15)
+ } else {
+ wallet_config.ext_descriptor.clone()
+ };
+ row.push(ext_desc_display.cell());
+
+ let has_int_desc = if wallet_config.int_descriptor.is_some() {
+ "Yes"
+ } else {
+ "No"
+ };
+ row.push(has_int_desc.cell());
+
+ rows.push(row);
+ }
+
+ let mut title_cells = vec!["Wallet Name".cell().bold(true), "Network".cell().bold(true)];
+
+ #[cfg(any(feature = "sqlite", feature = "redb"))]
+ title_cells.push("Database".cell().bold(true));
+
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+ ))]
+ title_cells.push("Client".cell().bold(true));
+
+ #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+ title_cells.push("Server URL".cell().bold(true));
+
+ title_cells.push("External Desc".cell().bold(true));
+ title_cells.push("Internal Desc".cell().bold(true));
+
+ let table = rows
+ .table()
+ .title(title_cells)
+ .display()
+ .map_err(|e| Error::Generic(e.to_string()))?;
+
+ Ok(format!("{table}"))
+ } else {
+ let wallets_summary: Vec<_> = config
+ .wallets
+ .iter()
+ .map(|(name, wallet_config)| {
+ let mut wallet_json = json!({
+ "name": name,
+ "network": wallet_config.network,
+ "ext_descriptor": wallet_config.ext_descriptor,
+ "int_descriptor": wallet_config.int_descriptor,
+ });
+
+ #[cfg(any(feature = "sqlite", feature = "redb"))]
+ {
+ wallet_json["database_type"] = json!(wallet_config.database_type.clone());
+ }
+
+ #[cfg(any(
+ feature = "electrum",
+ feature = "esplora",
+ feature = "rpc",
+ feature = "cbf"
+ ))]
+ {
+ wallet_json["client_type"] = json!(wallet_config.client_type.clone());
+ }
+
+ #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
+ {
+ wallet_json["server_url"] = json!(wallet_config.server_url.clone());
+ }
+
+ wallet_json
+ })
+ .collect();
+
+ Ok(serde_json::to_string_pretty(&json!({
+ "wallets": wallets_summary
+ }))?)
+ }
+}
+
/// The global top level handler.
pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
- let network = cli_opts.network;
let pretty = cli_opts.pretty;
let subcommand = cli_opts.subcommand.clone();
} => {
let home_dir = prepare_home_dir(cli_opts.datadir)?;
- let config = WalletConfig::load(&home_dir)?
- .ok_or(Error::Generic("No config found".to_string()))?;
- let wallet_opts = config.get_wallet_opts(&wallet)?;
+ let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet)?;
+
let database_path = prepare_wallet_db_dir(&home_dir, &wallet)?;
#[cfg(any(feature = "sqlite", feature = "redb"))]
}
};
- let mut wallet = new_persisted_wallet(network, &mut persister, wallet_opts)?;
- let blockchain_client = new_blockchain_client(wallet_opts, &wallet, database_path)?;
+ let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
+ let blockchain_client =
+ new_blockchain_client(&wallet_opts, &wallet, database_path)?;
let result = handle_online_wallet_subcommand(
&mut wallet,
wallet: wallet_name,
subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
} => {
- let network = cli_opts.network;
let datadir = cli_opts.datadir.clone();
let home_dir = prepare_home_dir(datadir)?;
- let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!(
- "No config found for wallet '{wallet_name}'"
- )))?;
- let wallet_opts = config.get_wallet_opts(&wallet_name)?;
+ let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
+
#[cfg(any(feature = "sqlite", feature = "redb"))]
let result = {
let mut persister: Persister = match &wallet_opts.database_type {
};
#[cfg(not(any(feature = "sqlite", feature = "redb")))]
let result = {
- let mut wallet = new_wallet(network, wallet_opts)?;
+ let mut wallet = new_wallet(network, &wallet_opts)?;
handle_offline_wallet_subcommand(
&mut wallet,
- wallet_opts,
+ &wallet_opts,
&cli_opts,
offline_subcommand.clone(),
)?
let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?;
Ok(result)
}
+ CliSubCommand::Wallets => {
+ let home_dir = prepare_home_dir(cli_opts.datadir)?;
+ let result = handle_wallets_subcommand(&home_dir, pretty)?;
+ Ok(result)
+ }
CliSubCommand::Key {
subcommand: key_subcommand,
} => {
+ let network = cli_opts.network;
let result = handle_key_subcommand(network, key_subcommand, pretty)?;
Ok(result)
}
policy,
script_type,
} => {
+ let network = cli_opts.network;
let result = handle_compile_subcommand(network, policy, script_type, pretty)?;
Ok(result)
}
#[cfg(feature = "repl")]
CliSubCommand::Repl {
wallet: wallet_name,
- mut wallet_opts,
} => {
- let network = cli_opts.network;
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
- wallet_opts.wallet = Some(wallet_name.clone());
-
- let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!(
- "No config found for wallet {}",
- wallet_name.clone()
- )))?;
- let loaded_wallet_opts = config.get_wallet_opts(&wallet_name)?;
+ let (wallet_opts, network) = load_wallet_config(&home_dir, &wallet_name)?;
#[cfg(any(feature = "sqlite", feature = "redb"))]
let (mut wallet, mut persister) = {
- let mut persister: Persister = match &loaded_wallet_opts.database_type {
+ let mut persister: Persister = match &wallet_opts.database_type {
#[cfg(feature = "sqlite")]
DatabaseType::Sqlite => {
let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
Persister::RedbStore(store)
}
};
- let wallet = new_persisted_wallet(network, &mut persister, &loaded_wallet_opts)?;
+ let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
(wallet, persister)
};
#[cfg(not(any(feature = "sqlite", feature = "redb")))]
use bdk_wallet::bitcoin::Network;
use log::{debug, error, warn};
-use std::env;
-use std::path::PathBuf;
use crate::commands::CliOpts;
-use crate::config::WalletConfig;
-use crate::error::BDKCliError as Error;
use crate::handlers::*;
-use crate::utils::prepare_home_dir;
use clap::Parser;
-fn is_value_arg(arg: &str) -> bool {
- matches!(
- arg,
- "-w" | "--wallet"
- | "-e"
- | "--ext-descriptor"
- | "-i"
- | "--int-descriptor"
- | "-c"
- | "--client-type"
- | "-d"
- | "--database-type"
- | "-u"
- | "--url"
- | "-b"
- | "--batch-size"
- | "-p"
- | "--parallel-requests"
- | "-a"
- | "--basic-auth"
- | "--cookie"
- | "-n"
- | "--network"
- | "--datadir"
- )
-}
-
-/// Inject configuration values from config.toml
-/// when --use-config is present, except for the init subcommand.
-fn preprocess_args(args: &mut Vec<String>) -> Result<(), Error> {
- let use_config = args.iter().any(|arg| arg == "--use-config");
-
- let is_init = args.iter().any(|arg| arg == "init");
-
- if !use_config || is_init {
- return Ok(());
- }
-
- let mut wallet_name: Option<String> = None;
- let mut datadir: Option<String> = None;
-
- let mut i = 1;
- while i < args.len() {
- if args[i] == "-w" || args[i] == "--wallet" {
- if i + 1 < args.len() {
- wallet_name = Some(args[i + 1].clone());
- }
- } else if (args[i] == "-d" || args[i] == "--datadir") && i + 1 < args.len() {
- datadir = Some(args[i + 1].clone());
- }
- i += if is_value_arg(&args[i]) && i + 1 < args.len() {
- 2
- } else {
- 1
- };
- }
-
- if let Some(wallet_name) = wallet_name {
- let home_dir = prepare_home_dir(datadir.map(PathBuf::from))?;
- if let Ok(Some(config)) = WalletConfig::load(&home_dir) {
- if let Some(wallet_config) = config.wallets.get(&wallet_name) {
- let mut top_level_injections: Vec<String> = Vec::new();
- let mut wallet_injections: Vec<String> = Vec::new();
-
- if !args.iter().any(|arg| arg == "-n" || arg == "--network") {
- top_level_injections.push("--network".to_string());
- top_level_injections.push(wallet_config.network.clone());
- }
-
- if !args
- .iter()
- .any(|arg| arg == "-e" || arg == "--ext-descriptor")
- {
- wallet_injections.push("--ext-descriptor".to_string());
- wallet_injections.push(wallet_config.ext_descriptor.clone());
- }
- if !args
- .iter()
- .any(|arg| arg == "-i" || arg == "--int-descriptor")
- {
- if let Some(int_descriptor) = &wallet_config.int_descriptor {
- wallet_injections.push("--int-descriptor".to_string());
- wallet_injections.push(int_descriptor.clone());
- }
- }
- #[cfg(any(
- feature = "electrum",
- feature = "esplora",
- feature = "rpc",
- feature = "cbf"
- ))]
- if !args.iter().any(|arg| arg == "-c" || arg == "--client-type") {
- if let Some(ct) = &wallet_config.client_type {
- wallet_injections.push("--client-type".to_string());
- wallet_injections.push(ct.clone());
- }
- }
- if !args
- .iter()
- .any(|arg| arg == "-d" || arg == "--database-type")
- {
- #[cfg(any(feature = "sqlite", feature = "redb"))]
- {
- wallet_injections.push("--database-type".to_string());
- wallet_injections.push(wallet_config.database_type.clone());
- }
- }
- #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
- if !args.iter().any(|arg| arg == "-u" || arg == "--url") {
- if let Some(url) = &wallet_config.server_url {
- wallet_injections.push("--url".to_string());
- wallet_injections.push(url.clone());
- }
- }
-
- let mut top_level_insert_pos = 1;
- while top_level_insert_pos < args.len()
- && args[top_level_insert_pos].starts_with('-')
- {
- if is_value_arg(&args[top_level_insert_pos])
- && top_level_insert_pos + 1 < args.len()
- {
- top_level_insert_pos += 2;
- } else {
- top_level_insert_pos += 1;
- }
- }
- args.splice(
- top_level_insert_pos..top_level_insert_pos,
- top_level_injections,
- );
-
- let wallet_pos = args
- .iter()
- .position(|arg| arg == "wallet")
- .unwrap_or(args.len());
- let mut wallet_insert_pos = wallet_pos + 1;
- while wallet_insert_pos < args.len() && args[wallet_insert_pos].starts_with('-') {
- if is_value_arg(&args[wallet_insert_pos]) && wallet_insert_pos + 1 < args.len()
- {
- wallet_insert_pos += 2;
- } else {
- wallet_insert_pos += 1;
- }
- }
- args.splice(wallet_insert_pos..wallet_insert_pos, wallet_injections);
- }
- }
- }
-
- Ok(())
-}
-
#[tokio::main]
async fn main() {
env_logger::init();
-
- let mut args: Vec<String> = env::args().collect();
-
- if let Err(e) = preprocess_args(&mut args) {
- error!("Failed to preprocess arguments: {e}");
- std::process::exit(1);
- }
-
- if let Some(pos) = args.iter().position(|arg| arg == "--use-config") {
- args.remove(pos);
- }
-
- let cli_opts: CliOpts = CliOpts::parse_from(args);
+ let cli_opts: CliOpts = CliOpts::parse();
let network = &cli_opts.network;
debug!("network: {network:?}");
//! Utility Tools
//!
//! This module includes all the utility tools used by the App.
+use crate::config::WalletConfig;
use crate::error::BDKCliError as Error;
use std::{
fmt::Display,
let int_descriptor = wallet_opts.int_descriptor.clone();
match int_descriptor {
- Some(int_descriptor) => {
+ Some(int_descriptor) => {
let wallet = Wallet::create(ext_descriptor, int_descriptor)
.network(network)
.create_wallet_no_persist()?;
Ok(wallet)
}
- None => {
+ None => {
let wallet = Wallet::create_single(ext_descriptor)
.network(network)
.create_wallet_no_persist()?;
pub(crate) fn shorten(displayable: impl Display, start: u8, end: u8) -> String {
let displayable = displayable.to_string();
+
+ if displayable.len() <= (start + end) as usize {
+ return displayable;
+ }
+
let start_str: &str = &displayable[0..start as usize];
let end_str: &str = &displayable[displayable.len() - end as usize..];
format!("{start_str}...{end_str}")
Ok(format!("{table}"))
}
+
+pub fn load_wallet_config(
+ home_dir: &Path,
+ wallet_name: &str,
+) -> Result<(WalletOpts, Network), Error> {
+ let config = WalletConfig::load(home_dir)?.ok_or(Error::Generic(format!(
+ "No config found for wallet {wallet_name}",
+ )))?;
+
+ let wallet_opts = config.get_wallet_opts(wallet_name)?;
+ let wallet_config = config
+ .wallets
+ .get(wallet_name)
+ .ok_or(Error::Generic(format!(
+ "Wallet '{wallet_name}' not found in config"
+ )))?;
+
+ let network = match wallet_config.network.as_str() {
+ "bitcoin" => Ok(Network::Bitcoin),
+ "testnet" => Ok(Network::Testnet),
+ "regtest" => Ok(Network::Regtest),
+ "signet" => Ok(Network::Signet),
+ "testnet4" => Ok(Network::Testnet4),
+ _ => Err(Error::Generic("Invalid network in config".to_string())),
+ }?;
+
+ Ok((wallet_opts, network))
+}