Skip to content

Commit eee4851

Browse files
committed
feat: add init wallet to app
- update Justfile to create and save values in config.toml file - update utils, commands and handlers files to use values from config.toml
1 parent 70170a2 commit eee4851

File tree

6 files changed

+147
-29
lines changed

6 files changed

+147
-29
lines changed

Justfile

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,32 @@ descriptors private wallet=default_wallet:
9999
# run any bitcoin-cli rpc command
100100
[group('rpc')]
101101
rpc command wallet=default_wallet:
102-
bitcoin-cli -datadir={{default_datadir}} -regtest -rpcwallet={{wallet}} -rpcuser={{rpc_user}} -rpcpassword={{rpc_password}} {{command}}
102+
bitcoin-cli -datadir={{default_datadir}} -regtest -rpcwallet={{wallet}} -rpcuser={{rpc_user}} -rpcpassword={{rpc_password}} {{command}}
103+
104+
[group('wallet')]
105+
init network wallet_name ext_descriptor int_descriptor client_type url database_type='sqlite' rpc_user='user' rpc_password='pass' 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 "network = \"{{network}}\"" >> {{default_datadir}}/config.toml
123+
echo "ext_descriptor = \"{{ext_descriptor}}\"" >> {{default_datadir}}/config.toml
124+
echo "int_descriptor = \"{{int_descriptor}}\"" >> {{default_datadir}}/config.toml
125+
echo "database_type = \"sqlite\"" >> {{default_datadir}}/config.toml
126+
echo "client_type = \"{{client_type}}\"" >> {{default_datadir}}/config.toml
127+
echo "server_url = \"{{url}}\"" >> {{default_datadir}}/config.toml
128+
echo "rpc_user = \"{{rpc_user}}\"" >> {{default_datadir}}/config.toml
129+
echo "rpc_password = \"{{rpc_password}}\"" >> {{default_datadir}}/config.toml
130+
echo "Wallet configuration for {{wallet_name}} added to {{default_datadir}}/config.toml"

src/commands.rs

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
1515
#![allow(clippy::large_enum_variant)]
1616

17+
use std::path::Path;
18+
use crate::config::WalletConfig;
19+
use crate::error::BDKCliError as Error;
1720
use bdk_wallet::bitcoin::{
1821
bip32::{DerivationPath, Xpriv},
1922
Address, Network, OutPoint, ScriptBuf,
@@ -167,15 +170,15 @@ pub struct WalletOpts {
167170
feature = "rpc",
168171
feature = "cbf"
169172
))]
170-
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum, required = true)]
171-
pub client_type: ClientType,
173+
#[arg(env = "CLIENT_TYPE", short = 'c', long, value_enum)]
174+
pub client_type: Option<ClientType>,
172175
#[cfg(feature = "sqlite")]
173-
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, required = true)]
174-
pub database_type: DatabaseType,
176+
#[arg(env = "DATABASE_TYPE", short = 'd', long, value_enum, default_value="sqlite")]
177+
pub database_type: Option<DatabaseType>,
175178
/// Sets the server url.
176179
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
177-
#[arg(env = "SERVER_URL", short = 'u', long, required = true)]
178-
pub url: String,
180+
#[arg(env = "SERVER_URL", short = 'u', long)]
181+
pub url: Option<String>,
179182
/// Electrum batch size.
180183
#[cfg(feature = "electrum")]
181184
#[arg(env = "ELECTRUM_BATCH_SIZE", short = 'b', long, default_value = "10")]
@@ -198,7 +201,7 @@ pub struct WalletOpts {
198201
value_parser = parse_proxy_auth,
199202
default_value = "user:password",
200203
)]
201-
pub basic_auth: (String, String),
204+
pub basic_auth: Option<(String, String)>,
202205
#[cfg(feature = "rpc")]
203206
/// Sets an optional cookie authentication.
204207
#[arg(env = "COOKIE")]
@@ -208,6 +211,63 @@ pub struct WalletOpts {
208211
pub compactfilter_opts: CompactFilterOpts,
209212
}
210213

214+
impl WalletOpts {
215+
/// Load configuration from TOML file and merge with CLI options
216+
pub fn load_config(&mut self, wallet_name: &str, datadir: &Path) -> Result<(), Error> {
217+
if let Some(config) = WalletConfig::load(datadir)? {
218+
if let Ok(config_opts) = config.get_wallet_opts(wallet_name) {
219+
self.wallet = self.wallet.take().or(config_opts.wallet);
220+
self.verbose = self.verbose || config_opts.verbose;
221+
self.ext_descriptor = self.ext_descriptor.take().or(config_opts.ext_descriptor);
222+
self.int_descriptor = self.int_descriptor.take().or(config_opts.int_descriptor);
223+
#[cfg(any(
224+
feature = "electrum",
225+
feature = "esplora",
226+
feature = "rpc",
227+
feature = "cbf"
228+
))]
229+
{
230+
self.client_type = self.client_type.clone().or(config_opts.client_type);
231+
}
232+
#[cfg(feature = "sqlite")]
233+
{
234+
self.database_type = self.database_type.clone().or(config_opts.database_type);
235+
}
236+
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
237+
{
238+
self.url = self.url.take().or(config_opts.url);
239+
}
240+
#[cfg(feature = "electrum")]
241+
{
242+
self.batch_size = if self.batch_size != 10 {
243+
config_opts.batch_size
244+
} else {
245+
self.batch_size
246+
};
247+
}
248+
#[cfg(feature = "esplora")]
249+
{
250+
self.parallel_requests = if self.parallel_requests != 5 {
251+
config_opts.parallel_requests
252+
} else {
253+
self.parallel_requests
254+
};
255+
}
256+
#[cfg(feature = "rpc")]
257+
{
258+
self.basic_auth = self.basic_auth.take().or(config_opts.basic_auth);
259+
self.cookie = self.cookie.take().or(config_opts.cookie);
260+
}
261+
#[cfg(feature = "cbf")]
262+
{
263+
self.compactfilter_opts = config_opts.compactfilter_opts;
264+
}
265+
}
266+
}
267+
Ok(())
268+
}
269+
}
270+
211271
/// Options to configure a SOCKS5 proxy for a blockchain client connection.
212272
#[cfg(any(feature = "electrum", feature = "esplora"))]
213273
#[derive(Debug, Args, Clone, PartialEq, Eq)]

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ pub enum BDKCliError {
3636
#[error("Hex conversion error: {0}")]
3737
HexToArrayError(#[from] bdk_wallet::bitcoin::hashes::hex::HexToArrayError),
3838

39+
#[error("Invalid network")]
40+
InvalidNetwork,
41+
3942
#[error("Key error: {0}")]
4043
KeyError(#[from] bdk_wallet::keys::KeyError),
4144

src/handlers.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -750,17 +750,19 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
750750
let network = cli_opts.network;
751751
let home_dir = prepare_home_dir(cli_opts.datadir)?;
752752
let wallet_name = &wallet_opts.wallet;
753-
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
753+
let database_path =
754+
prepare_wallet_db_dir(wallet_name, &home_dir, &mut wallet_opts.clone())?;
754755
#[cfg(feature = "sqlite")]
755756
let result = {
756757
let mut persister = match &wallet_opts.database_type {
757758
#[cfg(feature = "sqlite")]
758-
DatabaseType::Sqlite => {
759+
Some(DatabaseType::Sqlite) => {
759760
let db_file = database_path.join("wallet.sqlite");
760761
let connection = Connection::open(db_file)?;
761762
log::debug!("Sqlite database opened successfully");
762763
connection
763-
}
764+
},
765+
None => return Err(Error::Generic("Dataase type is required".to_string())),
764766
};
765767

766768
let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
@@ -796,15 +798,17 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
796798
let result = {
797799
let home_dir = prepare_home_dir(cli_opts.datadir)?;
798800
let wallet_name = &wallet_opts.wallet;
799-
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
801+
let database_path =
802+
prepare_wallet_db_dir(wallet_name, &home_dir, &mut wallet_opts.clone())?;
800803
let mut persister = match &wallet_opts.database_type {
801804
#[cfg(feature = "sqlite")]
802-
DatabaseType::Sqlite => {
805+
Some(DatabaseType::Sqlite) => {
803806
let db_file = database_path.join("wallet.sqlite");
804807
let connection = Connection::open(db_file)?;
805808
log::debug!("Sqlite database opened successfully");
806809
connection
807-
}
810+
},
811+
None => return Err(Error::Generic("Databas type is required".to_string())),
808812
};
809813

810814
let mut wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
@@ -846,25 +850,27 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result<String, Error> {
846850

847851
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
848852

849-
let database_path = prepare_wallet_db_dir(wallet_name, &home_dir)?;
853+
let database_path =
854+
prepare_wallet_db_dir(wallet_name, &home_dir, &mut wallet_opts.clone())?;
850855

851856
let mut persister = match &wallet_opts.database_type {
852857
#[cfg(feature = "sqlite")]
853-
DatabaseType::Sqlite => {
858+
Some(DatabaseType::Sqlite) => {
854859
let db_file = database_path.join("wallet.sqlite");
855860
let connection = Connection::open(db_file)?;
856861
log::debug!("Sqlite database opened successfully");
857862
connection
858-
}
863+
},
864+
None => return Err(Error::Generic("Database typ is required".to_string())),
859865
};
860866
let wallet = new_persisted_wallet(network, &mut persister, &wallet_opts)?;
861867
(wallet, persister)
862868
};
863869
#[cfg(not(any(feature = "sqlite")))]
864870
let mut wallet = new_wallet(network, &wallet_opts)?;
865871
let home_dir = prepare_home_dir(cli_opts.datadir.clone())?;
866-
let database_path = prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir)?;
867-
872+
let database_path =
873+
prepare_wallet_db_dir(&wallet_opts.wallet, &home_dir, &mut wallet_opts.clone())?;
868874
loop {
869875
let line = readline()?;
870876
let line = line.trim();

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#![warn(missing_docs)]
1212

1313
mod commands;
14+
mod config;
1415
mod error;
1516
mod handlers;
1617
mod utils;

src/utils.rs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,16 @@ pub(crate) fn prepare_home_dir(home_path: Option<PathBuf>) -> Result<PathBuf, Er
104104
pub(crate) fn prepare_wallet_db_dir(
105105
wallet_name: &Option<String>,
106106
home_path: &Path,
107+
wallet_opts: &mut WalletOpts,
107108
) -> Result<std::path::PathBuf, Error> {
108109
let mut dir = home_path.to_owned();
109110
if let Some(wallet_name) = wallet_name {
110111
dir.push(wallet_name);
111-
}
112112

113-
if !dir.exists() {
114-
std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
113+
if !dir.exists() {
114+
std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
115+
}
116+
wallet_opts.load_config(wallet_name, home_path)?;
115117
}
116118

117119
Ok(dir)
@@ -156,10 +158,13 @@ pub(crate) fn new_blockchain_client(
156158
_datadir: PathBuf,
157159
) -> Result<BlockchainClient, Error> {
158160
#[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]
159-
let url = wallet_opts.url.as_str();
161+
let url = wallet_opts
162+
.url
163+
.as_ref()
164+
.ok_or_else(|| Error::Generic("Server URL is required".to_string()))?;
160165
let client = match wallet_opts.client_type {
161166
#[cfg(feature = "electrum")]
162-
ClientType::Electrum => {
167+
Some(ClientType::Electrum) => {
163168
let client = bdk_electrum::electrum_client::Client::new(url)
164169
.map(bdk_electrum::BdkElectrumClient::new)?;
165170
BlockchainClient::Electrum {
@@ -168,7 +173,7 @@ pub(crate) fn new_blockchain_client(
168173
}
169174
}
170175
#[cfg(feature = "esplora")]
171-
ClientType::Esplora => {
176+
Some(ClientType::Esplora) => {
172177
let client = bdk_esplora::esplora_client::Builder::new(url).build_async()?;
173178
BlockchainClient::Esplora {
174179
client: Box::new(client),
@@ -177,12 +182,26 @@ pub(crate) fn new_blockchain_client(
177182
}
178183

179184
#[cfg(feature = "rpc")]
180-
ClientType::Rpc => {
185+
Some(ClientType::Rpc) => {
181186
let auth = match &wallet_opts.cookie {
182187
Some(cookie) => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::CookieFile(cookie.into()),
183188
None => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::UserPass(
184-
wallet_opts.basic_auth.0.clone(),
185-
wallet_opts.basic_auth.1.clone(),
189+
wallet_opts
190+
.basic_auth
191+
.as_ref()
192+
.ok_or_else(|| {
193+
Error::Generic("RPC authentication is required".to_string())
194+
})?
195+
.0
196+
.clone(),
197+
wallet_opts
198+
.basic_auth
199+
.as_ref()
200+
.ok_or_else(|| {
201+
Error::Generic("RPC authentication is required".to_string())
202+
})?
203+
.1
204+
.clone(),
186205
),
187206
};
188207
let client = bdk_bitcoind_rpc::bitcoincore_rpc::Client::new(url, auth)
@@ -193,7 +212,7 @@ pub(crate) fn new_blockchain_client(
193212
}
194213

195214
#[cfg(feature = "cbf")]
196-
ClientType::Cbf => {
215+
Some(ClientType::Cbf) => {
197216
let scan_type = match wallet_opts.compactfilter_opts.skip_blocks {
198217
Some(from_height) => Recovery { from_height },
199218
None => Sync,
@@ -210,6 +229,7 @@ pub(crate) fn new_blockchain_client(
210229
client: Box::new(client),
211230
}
212231
}
232+
None => return Err(Error::Generic("Client type is required".to_string())),
213233
};
214234
Ok(client)
215235
}

0 commit comments

Comments
 (0)