Skip to content

Commit fcf7ce8

Browse files
committed
feat: wallet config initialization
- 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 [Issue: #192]
1 parent 18335cf commit fcf7ce8

File tree

6 files changed

+217
-109
lines changed

6 files changed

+217
-109
lines changed

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 & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -195,68 +195,45 @@ Note: You can modify the `Justfile` to reflect your nodes' configuration values.
195195
cargo run --features rpc -- wallet -u "127.0.0.1:18443" -c rpc -a user:password balance
196196
```
197197

198-
### Initializing a Wallet with `just`
198+
### Initializing Wallet Configurations with `bdk-cli wallet init`
199199

200-
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.
201-
This eliminate the need to provide repetitive arguments. To set up a wallet with persistent configuration values, use the following just command:
200+
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.
202201

203-
```shell
204-
just init <wallet_name> <ext_descriptor> <int_descriptor> <database_type> <client_type> <url> <rpc_user> <rpc_password> [--force]
205-
```
206-
The arguments must be provided in the order shown above. Replace each placeholder with the corresponding value:
207-
208-
> * `wallet_name`: The unique name for your wallet (e.g., my_wallet)
209-
> * `ext_descriptor`: The external descriptor for generating receiving addresses (e.g., tr(tprv.../0/*)#checksum)
210-
> * `int_descriptor`: The internal descriptor for generating change addresses (e.g., tr(tprv.../1/*)#checksum)
211-
> * `database_type`: The database type for wallet persistence (e.g., sqlite). Defaults to `sqlite` if omitted
212-
> * `client_type`: The blockchain backend (e.g., electrum, esplora, rpc, cbf)
213-
> * `url`: The server URL for the blockchain backend (e.g., ssl://mempool.space:60602 for Electrum).
214-
> * `rpc_user`: The RPC username for rpc client type (e.g., user). Defaults to user if omitted.
215-
> * `rpc_password`: The RPC password for rpc client type (e.g., password). Defaults to password if omitted.
216-
> * `--force`: Optional. Overwrites existing configuration for the specified <wallet_name> if set. By default, `just init` fails if the wallet config values already exists.
217-
218-
#### Example
219-
220-
To initialize a wallet named `my_wallet` with `electrum` as the backend:
202+
To initialize a wallet configuration, use the following command structure:
221203

222204
```shell
223-
just init my_wallet "tr(tprv8Z.../0/*)#dtdqk3dx" "tr(tprv8Z.../1/*)#ulgptya7" sqlite electrum "ssl://mempool.space:60602" user password
205+
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>]
206+
[--rpc-password <rpc_password>]
224207
```
225208

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

228211
```shell
229-
just init-wallet my_wallet "tr(tprv8Z.../0/*)#dtdqk3dx" "tr(tprv8Z.../1/*)#ulgptya7" sqlite electrum "ssl://mempool.space:60602" user password --force
212+
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"
230213
```
231214

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

234-
`database_type`: Defaults to sqlite.
235-
`rpc_user`: Defaults to user.
236-
`rpc_password`: Defaults to password.
217+
You can omit the following arguments to use their default values:
237218

238-
For example, to initialize a wallet with default database_type, rpc_user, and rpc_password:
219+
`network`: Defaults to `testnet`
239220

240-
```shell
241-
just init-wallet my_wallet "tr(tprv8Z.../0/*)#dtdqk3dx" "tr(tprv8Z.../1/*)#ulgptya7" electrum "ssl://mempool.space:60602"
242-
```
221+
`database_type`: Defaults to `sqlite`
243222

244223
#### Using Saved Configuration
245224

246-
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.
225+
After a wallet is initialized, you can then run `bdk-cli` wallet commands without specifying the parameters, referencing only the wallet subcommand.
247226

248-
With the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow:
227+
For example, with the wallet `my_wallet` initialized, generate a new address and sync the wallet as follow:
249228

250229
```shell
251-
cargo run --features electrum,sqlite -- -n signet wallet -w my_wallet new_address
230+
cargo run wallet -w my_wallet new_address
252231

253-
cargo run --features electrum,sqlite -- -n signet wallet -w my_wallet sync
232+
cargo run --features electrum wallet -w my_wallet sync
254233
```
255234

256-
#### Notes:
235+
Note that each wallet has its own configuration, allowing multiple wallets with different configurations.
257236

258-
* Each wallet has its own configuration, allowing multiple wallets with different settings (e.g., different descriptors or backends).
259-
* 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`).
260237

261238
## Minimum Supported Rust Version (MSRV)
262239

src/commands.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ pub enum CliSubCommand {
112112
/// Wallet operation subcommands.
113113
#[derive(Debug, Subcommand, Clone, PartialEq)]
114114
pub enum WalletSubCommand {
115+
/// Initialize a wallet configuration and save to `config.toml`.
116+
Init {
117+
#[command(flatten)]
118+
wallet_opts: WalletOpts,
119+
/// Overwrite existing wallet configuration if it exists.
120+
#[arg(long = "force", default_value_t = false)]
121+
force: bool,
122+
},
115123
#[cfg(any(
116124
feature = "electrum",
117125
feature = "esplora",
@@ -173,13 +181,7 @@ pub struct WalletOpts {
173181
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum)]
174182
pub client_type: Option<ClientType>,
175183
#[cfg(feature = "sqlite")]
176-
#[arg(
177-
env = "DATABASE_TYPE",
178-
short = 'd',
179-
long,
180-
value_enum,
181-
default_value = "sqlite"
182-
)]
184+
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum)]
183185
pub database_type: Option<DatabaseType>,
184186
/// Sets the server url.
185187
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]

src/config.rs

Lines changed: 54 additions & 30 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,26 +52,53 @@ 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
100+
"Invalid database type: {database_type}",
101+
database_type = wallet_config.database_type
74102
)))
75103
}
76104
};
@@ -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)