Skip to content

Commit 512b4b5

Browse files
committed
feat(wallet-init): add list command
- add wallet list command - add warning for using priv descriptors - update readme
1 parent 7e4d0e0 commit 512b4b5

File tree

7 files changed

+205
-206
lines changed

7 files changed

+205
-206
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ Changelog info is also documented on the [GitHub releases](https://github.com/bi
44
page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
55

66
## [Unreleased]
7-
- Add wallet configs initialization for initialiazing and saving wallet configs
87
- Add wallet subcommand `config` to save wallet configs
8+
- Add wallet subcommand `list` to display saved configs
99

1010
## [2.0.0]
1111

README.md

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -203,41 +203,34 @@ cargo run --pretty -n signet wallet -w {wallet_name} -d sqlite balance
203203
```
204204
This is available for wallet, key, repl and compile features. When ommitted, outputs default to `JSON`.
205205

206-
## Initializing Wallet Configurations with `init` Subcommand
206+
## Initializing and saving wallet configurations
207207

208-
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.
208+
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.
209209

210-
To initialize a wallet configuration, use the following command structure:
210+
To save a wallet settings:
211211

212212
```shell
213-
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>]
214-
[--rpc-password <rpc_password>] init
213+
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>]
214+
[--rpc-password <rpc_password>]
215215
```
216216

217217
For example, to initialize a wallet named `my_wallet` with `electrum` as the backend on `signet` network:
218218

219219
```shell
220-
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
220+
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"
221221
```
222222

223-
To overwrite an existing wallet configuration, use the `--force` flag after the `init` sub-command.
223+
To overwrite an existing wallet configuration, use the `--force` flag after the `config` sub-command.
224224

225-
You can omit the following arguments to use their default values:
225+
#### Using a Configured Wallet
226226

227-
`network`: Defaults to `testnet`
227+
Once configured, use any wallet command with just the wallet name:
228228

229-
`database_type`: Defaults to `sqlite`
230-
231-
#### Using Saved Configuration
232-
233-
After a wallet is initialized, you can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet subcommand.
234-
235-
For example, with the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow:
236229

237230
```shell
238-
cargo run wallet -w my_wallet --use-config new_address
231+
cargo run --features electrum wallet -w my_wallet new_address
239232

240-
cargo run --features electrum wallet -w my_wallet --use-config sync
233+
cargo run --features electrum wallet -w my_wallet full_scan
241234
```
242235

243236
Note that each wallet has its own configuration, allowing multiple wallets with different configurations.

src/commands.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,6 @@ pub enum CliSubCommand {
108108
/// Wallet name for this REPL session
109109
#[arg(env = "WALLET_NAME", short = 'w', long = "wallet", required = true)]
110110
wallet: String,
111-
112-
#[command(flatten)]
113-
wallet_opts: WalletOpts,
114111
},
115112
}
116113

@@ -136,6 +133,8 @@ pub enum WalletSubCommand {
136133
OnlineWalletSubCommand(OnlineWalletSubCommand),
137134
#[command(flatten)]
138135
OfflineWalletSubCommand(OfflineWalletSubCommand),
136+
/// List all saved wallet configurations.
137+
List,
139138
}
140139

141140
#[derive(Clone, ValueEnum, Debug, Eq, PartialEq)]

src/config.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ impl WalletConfig {
105105
return Err(Error::Generic("Invalid database type".to_string()));
106106
}
107107
};
108-
109108
#[cfg(any(
110109
feature = "electrum",
111110
feature = "esplora",
@@ -144,9 +143,9 @@ impl WalletConfig {
144143
.clone()
145144
.ok_or_else(|| Error::Generic(format!("Server url not found")))?,
146145
#[cfg(feature = "electrum")]
147-
batch_size: 10,
146+
batch_size: wallet_config.batch_size.unwrap_or(10),
148147
#[cfg(feature = "esplora")]
149-
parallel_requests: 5,
148+
parallel_requests: wallet_config.parallel_requests.unwrap_or(5),
150149
#[cfg(feature = "rpc")]
151150
basic_auth: (
152151
wallet_config

src/handlers.rs

Lines changed: 181 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,35 @@ pub fn handle_config_subcommand(
876876
wallet_opts: &WalletOpts,
877877
force: bool,
878878
) -> Result<String, Error> {
879+
if network == Network::Bitcoin {
880+
eprintln!(
881+
"\n⚠️ WARNING: You are configuring a wallet for Bitcoin MAINNET.\n\
882+
This software is experimental and not recommended for use with real funds.\n\
883+
Consider using a testnet for testing purposes. \n"
884+
);
885+
}
886+
887+
let ext_descriptor = wallet_opts.ext_descriptor.clone();
888+
let int_descriptor = wallet_opts.int_descriptor.clone();
889+
890+
if ext_descriptor.contains("xprv") || ext_descriptor.contains("tprv") {
891+
eprintln!(
892+
"\n⚠️ WARNING: Your external descriptor contains PRIVATE KEYS.\n\
893+
Private keys will be saved in PLAINTEXT in the config file.\n\
894+
This is a security risk. Consider using public descriptors instead."
895+
);
896+
}
897+
898+
if let Some(ref internal_desc) = int_descriptor {
899+
if internal_desc.contains("xprv") || internal_desc.contains("tprv") {
900+
eprintln!(
901+
"\n⚠️ WARNING: Your internal descriptor contains PRIVATE KEYS.\n\
902+
Private keys will be saved in PLAINTEXT in the config file.\n\
903+
This is a security risk. Consider using public descriptors instead."
904+
);
905+
}
906+
}
907+
879908
let mut config = WalletConfig::load(datadir)?.unwrap_or(WalletConfig {
880909
network,
881910
wallets: HashMap::new(),
@@ -887,8 +916,6 @@ pub fn handle_config_subcommand(
887916
)));
888917
}
889918

890-
let ext_descriptor = wallet_opts.ext_descriptor.clone();
891-
let int_descriptor = wallet_opts.int_descriptor.clone();
892919
#[cfg(any(
893920
feature = "electrum",
894921
feature = "esplora",
@@ -1160,6 +1187,139 @@ pub(crate) fn handle_compile_subcommand(
11601187
}
11611188
}
11621189

1190+
/// Handle list subcommand to show all saved wallet configurations
1191+
pub fn handle_list_subcommand(datadir: &Path, pretty: bool) -> Result<String, Error> {
1192+
let load_config = WalletConfig::load(datadir)?;
1193+
1194+
let config = match load_config {
1195+
Some(c) if !c.wallets.is_empty() => c,
1196+
_ => {
1197+
return Ok(if pretty {
1198+
"No wallet configurations found.".to_string()
1199+
} else {
1200+
serde_json::to_string_pretty(&json!({
1201+
"wallets": []
1202+
}))?
1203+
});
1204+
}
1205+
};
1206+
1207+
if pretty {
1208+
let mut rows: Vec<Vec<CellStruct>> = vec![];
1209+
1210+
for (name, wallet_config) in config.wallets.iter() {
1211+
let mut row = vec![name.cell(), wallet_config.network.clone().cell()];
1212+
1213+
#[cfg(any(feature = "sqlite", feature = "redb"))]
1214+
row.push(wallet_config.database_type.clone().cell());
1215+
1216+
#[cfg(any(
1217+
feature = "electrum",
1218+
feature = "esplora",
1219+
feature = "rpc",
1220+
feature = "cbf"
1221+
))]
1222+
{
1223+
let client_str = wallet_config.client_type.as_deref().unwrap_or("N/A");
1224+
row.push(client_str.cell());
1225+
}
1226+
1227+
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
1228+
{
1229+
let url_str = wallet_config.server_url.as_deref().unwrap_or("N/A");
1230+
let display_url = if url_str.len() > 20 {
1231+
shorten(url_str, 15, 10)
1232+
} else {
1233+
url_str.to_string()
1234+
};
1235+
row.push(display_url.cell());
1236+
}
1237+
1238+
let ext_desc_display = if wallet_config.ext_descriptor.len() > 40 {
1239+
shorten(&wallet_config.ext_descriptor, 20, 15)
1240+
} else {
1241+
wallet_config.ext_descriptor.clone()
1242+
};
1243+
row.push(ext_desc_display.cell());
1244+
1245+
let has_int_desc = if wallet_config.int_descriptor.is_some() {
1246+
"Yes"
1247+
} else {
1248+
"No"
1249+
};
1250+
row.push(has_int_desc.cell());
1251+
1252+
rows.push(row);
1253+
}
1254+
1255+
let mut title_cells = vec!["Wallet Name".cell().bold(true), "Network".cell().bold(true)];
1256+
1257+
#[cfg(any(feature = "sqlite", feature = "redb"))]
1258+
title_cells.push("Database".cell().bold(true));
1259+
1260+
#[cfg(any(
1261+
feature = "electrum",
1262+
feature = "esplora",
1263+
feature = "rpc",
1264+
feature = "cbf"
1265+
))]
1266+
title_cells.push("Client".cell().bold(true));
1267+
1268+
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
1269+
title_cells.push("Server URL".cell().bold(true));
1270+
1271+
title_cells.push("External Desc".cell().bold(true));
1272+
title_cells.push("Internal Desc".cell().bold(true));
1273+
1274+
let table = rows
1275+
.table()
1276+
.title(title_cells)
1277+
.display()
1278+
.map_err(|e| Error::Generic(e.to_string()))?;
1279+
1280+
Ok(format!("{table}"))
1281+
} else {
1282+
let wallets_summary: Vec<_> = config
1283+
.wallets
1284+
.iter()
1285+
.map(|(name, wallet_config)| {
1286+
let mut wallet_json = json!({
1287+
"name": name,
1288+
"network": wallet_config.network,
1289+
"ext_descriptor": wallet_config.ext_descriptor,
1290+
"int_descriptor": wallet_config.int_descriptor,
1291+
});
1292+
1293+
#[cfg(any(feature = "sqlite", feature = "redb"))]
1294+
{
1295+
wallet_json["database_type"] = json!(wallet_config.database_type.clone());
1296+
}
1297+
1298+
#[cfg(any(
1299+
feature = "electrum",
1300+
feature = "esplora",
1301+
feature = "rpc",
1302+
feature = "cbf"
1303+
))]
1304+
{
1305+
wallet_json["client_type"] = json!(wallet_config.client_type.clone());
1306+
}
1307+
1308+
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
1309+
{
1310+
wallet_json["server_url"] = json!(wallet_config.server_url.clone());
1311+
}
1312+
1313+
wallet_json
1314+
})
1315+
.collect();
1316+
1317+
Ok(serde_json::to_string_pretty(&json!({
1318+
"wallets": wallets_summary
1319+
}))?)
1320+
}
1321+
}
1322+
11631323
/// The global top level handler.
11641324
pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
11651325
let network = cli_opts.network;
@@ -1298,6 +1458,14 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
12981458
let result = handle_config_subcommand(&home_dir, network, wallet, &wallet_opts, force)?;
12991459
Ok(result)
13001460
}
1461+
CliSubCommand::Wallet {
1462+
wallet: _,
1463+
subcommand: WalletSubCommand::List,
1464+
} => {
1465+
let home_dir = prepare_home_dir(cli_opts.datadir)?;
1466+
let result = handle_list_subcommand(&home_dir, pretty)?;
1467+
Ok(result)
1468+
}
13011469
CliSubCommand::Key {
13021470
subcommand: key_subcommand,
13031471
} => {
@@ -1315,21 +1483,19 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
13151483
#[cfg(feature = "repl")]
13161484
CliSubCommand::Repl {
13171485
wallet: wallet_name,
1318-
mut wallet_opts,
13191486
} => {
13201487
let network = cli_opts.network;
13211488
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
1322-
wallet_opts.wallet = Some(wallet_name.clone());
13231489

13241490
let config = WalletConfig::load(&home_dir)?.ok_or(Error::Generic(format!(
1325-
"No config found for wallet {}",
1491+
"No config found for wallet {}, please run 'config' command first.",
13261492
wallet_name.clone()
13271493
)))?;
1328-
let loaded_wallet_opts = config.get_wallet_opts(&wallet_name)?;
1494+
let wallet_opts = config.get_wallet_opts(&wallet_name)?;
13291495

13301496
#[cfg(any(feature = "sqlite", feature = "redb"))]
13311497
let (mut wallet, mut persister) = {
1332-
let mut persister: Persister = match &loaded_wallet_opts.database_type {
1498+
let mut persister: Persister = match &wallet_opts.database_type {
13331499
#[cfg(feature = "sqlite")]
13341500
DatabaseType::Sqlite => {
13351501
let database_path = prepare_wallet_db_dir(&home_dir, &wallet_name)?;
@@ -1348,7 +1514,7 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
13481514
Persister::RedbStore(store)
13491515
}
13501516
};
1351-
let wallet = new_persisted_wallet(network, &mut persister, &loaded_wallet_opts)?;
1517+
let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
13521518
(wallet, persister)
13531519
};
13541520
#[cfg(not(any(feature = "sqlite", feature = "redb")))]
@@ -1448,6 +1614,13 @@ async fn respond(
14481614
.map_err(|e| e.to_string())?;
14491615
Some(value)
14501616
}
1617+
ReplSubCommand::Wallet {
1618+
subcommand: WalletSubCommand::List,
1619+
} => {
1620+
let value =
1621+
handle_list_subcommand(&_datadir, cli_opts.pretty).map_err(|e| e.to_string())?;
1622+
Some(value)
1623+
}
14511624
ReplSubCommand::Key { subcommand } => {
14521625
let value = handle_key_subcommand(network, subcommand, cli_opts.pretty)
14531626
.map_err(|e| e.to_string())?;

0 commit comments

Comments
 (0)