Skip to content

Commit 9461e85

Browse files
committed
feat(wallet): add initializing wallet config
- update wallet initialization to use `bdk-cli wallet init` command directly and read walletopts - update README for how to use the bdk-cli wallet init command - fix clippy warnings - update CHANGELOG [Issue: #192]
1 parent a8d766e commit 9461e85

File tree

6 files changed

+241
-116
lines changed

6 files changed

+241
-116
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
77

88
- Removed MSRV and bumped Rust Edition to 2024
99
- Add `--pretty` top level flag for formatting commands output in a tabular format
10+
- Add wallet configs initialization for initialiazing and saving wallet configs
1011

1112
## [1.0.0]
1213

Justfile

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,30 +100,3 @@ descriptors private wallet=default_wallet:
100100
[group('rpc')]
101101
rpc command wallet=default_wallet:
102102
bitcoin-cli -datadir={{default_datadir}} -regtest -rpcwallet={{wallet}} -rpcuser={{rpc_user}} -rpcpassword={{rpc_password}} {{command}}
103-
104-
[group('wallet')]
105-
init wallet_name ext_descriptor int_descriptor client_type url database_type='sqlite' rpc_user='user' rpc_password='password' force='false':
106-
mkdir -p {{default_datadir}}
107-
# Check if wallet configuration exists
108-
if [ "{{force}}" = "false" ] && grep -Fx "[wallets.{{wallet_name}}]" {{default_datadir}}/config.toml > /dev/null; then \
109-
echo "Error: Wallet '{{wallet_name}}' already configured in {{default_datadir}}/config.toml. Use --force to overwrite."; \
110-
exit 1; \
111-
fi
112-
# Remove existing configuration if --force is true
113-
if [ "{{force}}" = "true" ] && grep -Fx "[wallets.{{wallet_name}}]" {{default_datadir}}/config.toml > /dev/null; then \
114-
sed -i.bak '/^\[wallets\.{{wallet_name}}\]/,/^\[/d' {{default_datadir}}/config.toml; \
115-
sed -i.bak '/^\[wallets\.{{wallet_name}}\]/d' {{default_datadir}}/config.toml; \
116-
rm {{default_datadir}}/config.toml.bak; \
117-
fi
118-
# Append new configuration
119-
echo "" >> {{default_datadir}}/config.toml || touch {{default_datadir}}/config.toml
120-
echo "[wallets.{{wallet_name}}]" >> {{default_datadir}}/config.toml
121-
echo "name = \"{{wallet_name}}\"" >> {{default_datadir}}/config.toml
122-
echo "ext_descriptor = \"{{ext_descriptor}}\"" >> {{default_datadir}}/config.toml
123-
echo "int_descriptor = \"{{int_descriptor}}\"" >> {{default_datadir}}/config.toml
124-
echo "database_type = \"sqlite\"" >> {{default_datadir}}/config.toml
125-
echo "client_type = \"{{client_type}}\"" >> {{default_datadir}}/config.toml
126-
echo "server_url = \"{{url}}\"" >> {{default_datadir}}/config.toml
127-
echo "rpc_user = \"{{rpc_user}}\"" >> {{default_datadir}}/config.toml
128-
echo "rpc_password = \"{{rpc_password}}\"" >> {{default_datadir}}/config.toml
129-
echo "Wallet configuration for {{wallet_name}} added to {{default_datadir}}/config.toml"

README.md

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -202,65 +202,45 @@ You can optionally return outputs of commands in human-readable, tabular format
202202
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`.
205-
### Initializing a Wallet with `just`
206205

207-
When using `bdk-cli`, repeatedly specifying parameter values for each command can be tedious. The `just` command allows you to initialize a wallet with configuration values once, saving them for reuse in subsequent `bdk-cli` commands.
208-
This eliminate the need to provide repetitive arguments. To set up a wallet with persistent configuration values, use the following just command:
206+
### Initializing Wallet Configurations with `bdk-cli wallet init`
209207

210-
```shell
211-
just init <wallet_name> <ext_descriptor> <int_descriptor> <database_type> <client_type> <url> <rpc_user> <rpc_password> [--force]
212-
```
213-
The arguments must be provided in the order shown above. Replace each placeholder with the corresponding value:
214-
215-
> * `wallet_name`: The unique name for your wallet (e.g., my_wallet)
216-
> * `ext_descriptor`: The external descriptor for generating receiving addresses (e.g., tr(tprv.../0/*)#checksum)
217-
> * `int_descriptor`: The internal descriptor for generating change addresses (e.g., tr(tprv.../1/*)#checksum)
218-
> * `database_type`: The database type for wallet persistence (e.g., sqlite). Defaults to `sqlite` if omitted
219-
> * `client_type`: The blockchain backend (e.g., electrum, esplora, rpc, cbf)
220-
> * `url`: The server URL for the blockchain backend (e.g., ssl://mempool.space:60602 for Electrum).
221-
> * `rpc_user`: The RPC username for rpc client type (e.g., user). Defaults to user if omitted.
222-
> * `rpc_password`: The RPC password for rpc client type (e.g., password). Defaults to password if omitted.
223-
> * `--force`: Optional. Overwrites existing configuration for the specified <wallet_name> if set. By default, `just init` fails if the wallet config values already exists.
224-
225-
#### Example
208+
The `bdk-cli wallet init` command simplifies wallet setup 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.
226209

227-
To initialize a wallet named `my_wallet` with `electrum` as the backend:
210+
To initialize a wallet configuration, use the following command structure:
228211

229212
```shell
230-
just init my_wallet "tr(tprv8Z.../0/*)#dtdqk3dx" "tr(tprv8Z.../1/*)#ulgptya7" sqlite electrum "ssl://mempool.space:60602" user password
213+
cargo run --features <list-of-features> -- -n <network> wallet init --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>]
231215
```
232216

233-
To overwrite an existing wallet configuration:
217+
For example, to initialize a wallet named `my_wallet` with `electrum` as the backend on `signet` network:
234218

235219
```shell
236-
just init-wallet my_wallet "tr(tprv8Z.../0/*)#dtdqk3dx" "tr(tprv8Z.../1/*)#ulgptya7" sqlite electrum "ssl://mempool.space:60602" user password --force
220+
cargo run --features electrum -- -n signet wallet init -w my_wallet -e "tr(tprv8Z.../0/*)#dtdqk3dx" -i "tr(tprv8Z.../1/*)#ulgptya7" -d sqlite -c electrum -u "ssl://mempool.space:60602"
237221
```
238222

239-
You can omit the following arguments to use their default values:
223+
To overwrite an existing wallet configuration, use the `--force` flag at the end of the command.
240224

241-
`database_type`: Defaults to sqlite.
242-
`rpc_user`: Defaults to user.
243-
`rpc_password`: Defaults to password.
225+
You can omit the following arguments to use their default values:
244226

245-
For example, to initialize a wallet with default database_type, rpc_user, and rpc_password:
227+
`network`: Defaults to `testnet`
246228

247-
```shell
248-
just init-wallet my_wallet "tr(tprv8Z.../0/*)#dtdqk3dx" "tr(tprv8Z.../1/*)#ulgptya7" electrum "ssl://mempool.space:60602"
249-
```
229+
`database_type`: Defaults to `sqlite`
250230

251231
#### Using Saved Configuration
252232

253-
After initializing a wallet with `just init`, the configuration is saved in `~/.bdk-bitcoin/config.toml`. You can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet name and network.
233+
After a wallet is initialized, you can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet subcommand.
254234

255-
With the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow:
235+
For example, with the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow:
256236

257237
```shell
258-
cargo run --features electrum,sqlite -- -n signet wallet -w my_wallet new_address
238+
cargo run wallet -w my_wallet new_address
259239

260-
cargo run --features electrum,sqlite -- -n signet wallet -w my_wallet sync
240+
cargo run --features electrum wallet -w my_wallet sync
261241
```
262242

263-
#### Notes:
243+
Note that each wallet has its own configuration, allowing multiple wallets with different configurations.
264244

265245
* Each wallet has its own configuration, allowing multiple wallets with different settings (e.g., different descriptors or backends).
266246
* You can override saved configuration values for a single command by specifying them explicitly (e.g., `--client-type esplora` or `--url https://mempool.space/signet/api`).

src/commands.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use bdk_wallet::bitcoin::{
2020
Address, Network, OutPoint, ScriptBuf,
2121
bip32::{DerivationPath, Xpriv},
2222
};
23-
use clap::{value_parser, Args, Parser, Subcommand, ValueEnum};
23+
use clap::{Args, Parser, Subcommand, ValueEnum, value_parser};
2424
use std::path::Path;
2525

2626
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
@@ -115,6 +115,14 @@ pub enum CliSubCommand {
115115
/// Wallet operation subcommands.
116116
#[derive(Debug, Subcommand, Clone, PartialEq)]
117117
pub enum WalletSubCommand {
118+
/// Initialize a wallet configuration and save to `config.toml`.
119+
Init {
120+
#[command(flatten)]
121+
wallet_opts: WalletOpts,
122+
/// Overwrite existing wallet configuration if it exists.
123+
#[arg(long = "force", default_value_t = false)]
124+
force: bool,
125+
},
118126
#[cfg(any(
119127
feature = "electrum",
120128
feature = "esplora",
@@ -176,11 +184,11 @@ pub struct WalletOpts {
176184
feature = "rpc",
177185
feature = "cbf"
178186
))]
179-
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum, required = true)]
180-
pub client_type: ClientType,
187+
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum)]
188+
pub client_type: Option<ClientType>,
181189
#[cfg(any(feature = "sqlite", feature = "redb"))]
182-
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, required = true)]
183-
pub database_type: DatabaseType,
190+
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum)]
191+
pub database_type: Option<DatabaseType>,
184192
/// Sets the server url.
185193
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
186194
#[arg(env = "SERVER_URL", short = 'u', long)]
@@ -235,7 +243,7 @@ impl WalletOpts {
235243
{
236244
self.client_type = self.client_type.clone().or(config_opts.client_type);
237245
}
238-
#[cfg(feature = "sqlite")]
246+
#[cfg(any(feature = "sqlite", feature = "redb"))]
239247
{
240248
// prioritizing the config.toml value for database type as it has a default value
241249
self.database_type = config_opts.database_type.or(self.database_type.clone());

src/config.rs

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,39 @@
55
feature = "cbf"
66
))]
77
use crate::commands::ClientType;
8-
use crate::commands::{DatabaseType, WalletOpts};
8+
#[cfg(feature = "sqlite")]
9+
use crate::commands::DatabaseType;
10+
use crate::commands::WalletOpts;
911
use crate::error::BDKCliError as Error;
10-
use serde::Deserialize;
12+
use bdk_wallet::bitcoin::Network;
13+
use serde::{Deserialize, Serialize};
1114
use std::collections::HashMap;
1215
use std::fs;
1316
use std::path::Path;
1417

15-
#[derive(Debug, Deserialize)]
18+
#[derive(Debug, Serialize, Deserialize)]
1619
pub struct WalletConfig {
20+
pub network: Network,
1721
pub wallets: HashMap<String, WalletConfigInner>,
1822
}
1923

20-
#[derive(Debug, Deserialize)]
24+
#[derive(Debug, Serialize, Deserialize)]
2125
pub struct WalletConfigInner {
2226
pub name: String,
27+
pub network: String,
2328
pub ext_descriptor: String,
2429
pub int_descriptor: String,
30+
#[cfg(feature = "sqlite")]
2531
pub database_type: String,
2632
#[cfg(any(
2733
feature = "electrum",
2834
feature = "esplora",
2935
feature = "rpc",
3036
feature = "cbf"
3137
))]
32-
pub client_type: String,
33-
#[cfg(any(
34-
feature = "electrum",
35-
feature = "esplora",
36-
feature = "rpc",
37-
feature = "cbf"
38-
))]
39-
pub server_url: String,
38+
pub client_type: Option<String>,
39+
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
40+
pub server_url: Option<String>,
4041
#[cfg(feature = "rpc")]
4142
pub rpc_user: String,
4243
#[cfg(feature = "rpc")]
@@ -51,27 +52,54 @@ impl WalletConfig {
5152
return Ok(None);
5253
}
5354
let config_content = fs::read_to_string(&config_path)
54-
.map_err(|e| Error::Generic(format!("Failed to read config file: {}", e)))?;
55+
.map_err(|e| Error::Generic(format!("Failed to read config file: {e}")))?;
5556
let config: WalletConfig = toml::from_str(&config_content)
56-
.map_err(|e| Error::Generic(format!("Failed to parse config file: {}", e)))?;
57+
.map_err(|e| Error::Generic(format!("Failed to parse config file: {e}")))?;
5758
Ok(Some(config))
5859
}
5960

61+
/// Save configuration to a TOML file
62+
pub fn save(&self, datadir: &Path) -> Result<(), Error> {
63+
let config_path = datadir.join("config.toml");
64+
let config_content = toml::to_string_pretty(self)
65+
.map_err(|e| Error::Generic(format!("Failed to serialize config: {e}")))?;
66+
fs::create_dir_all(datadir)
67+
.map_err(|e| Error::Generic(format!("Failed to create directory {datadir:?}: {e}")))?;
68+
fs::write(&config_path, config_content).map_err(|e| {
69+
Error::Generic(format!("Failed to write config file {config_path:?}: {e}"))
70+
})?;
71+
log::debug!("Saved config to {config_path:?}");
72+
Ok(())
73+
}
74+
6075
/// Get config for a wallet
6176
pub fn get_wallet_opts(&self, wallet_name: &str) -> Result<WalletOpts, Error> {
6277
let wallet_config = self
6378
.wallets
6479
.get(wallet_name)
65-
.ok_or_else(|| Error::Generic(format!("Wallet {} not found in config", wallet_name)))?;
80+
.ok_or_else(|| Error::Generic(format!("Wallet {wallet_name} not found in config")))?;
81+
82+
let _network = match wallet_config.network.as_str() {
83+
"bitcoin" => Network::Bitcoin,
84+
"testnet" => Network::Testnet,
85+
"regtest" => Network::Regtest,
86+
"signet" => Network::Signet,
87+
_ => {
88+
return Err(Error::Generic(format!(
89+
"Invalid network: {network}",
90+
network = wallet_config.network
91+
)));
92+
}
93+
};
6694

6795
#[cfg(feature = "sqlite")]
6896
let database_type = match wallet_config.database_type.as_str() {
6997
"sqlite" => DatabaseType::Sqlite,
7098
_ => {
7199
return Err(Error::Generic(format!(
72-
"Invalid database type: {}",
73-
wallet_config.database_type
74-
)))
100+
"Invalid database type: {database_type}",
101+
database_type = wallet_config.database_type
102+
)));
75103
}
76104
};
77105

@@ -81,21 +109,17 @@ impl WalletConfig {
81109
feature = "rpc",
82110
feature = "cbf"
83111
))]
84-
let client_type = match wallet_config.client_type.as_str() {
112+
let client_type = match wallet_config.client_type.as_deref() {
85113
#[cfg(feature = "electrum")]
86-
"electrum" => ClientType::Electrum,
114+
Some("electrum") => Some(ClientType::Electrum),
87115
#[cfg(feature = "esplora")]
88-
"esplora" => ClientType::Esplora,
116+
Some("esplora") => Some(ClientType::Esplora),
89117
#[cfg(feature = "rpc")]
90-
"rpc" => ClientType::Rpc,
118+
Some("rpc") => Some(ClientType::Rpc),
91119
#[cfg(feature = "cbf")]
92-
"cbf" => ClientType::Cbf,
93-
_ => {
94-
return Err(Error::Generic(format!(
95-
"Invalid client type: {}",
96-
wallet_config.client_type
97-
)))
98-
}
120+
Some("cbf") => Some(ClientType::Cbf),
121+
Some(other) => return Err(Error::Generic(format!("Invalid client type: {other}"))),
122+
None => None,
99123
};
100124

101125
Ok(WalletOpts {
@@ -109,11 +133,11 @@ impl WalletConfig {
109133
feature = "rpc",
110134
feature = "cbf"
111135
))]
112-
client_type: Some(client_type),
136+
client_type,
113137
#[cfg(feature = "sqlite")]
114138
database_type: Some(database_type),
115139
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
116-
url: Some(wallet_config.server_url.clone()),
140+
url: wallet_config.server_url.clone(),
117141
#[cfg(feature = "electrum")]
118142
batch_size: 10,
119143
#[cfg(feature = "esplora")]

0 commit comments

Comments
 (0)