From f6a183f73963fce7aa6ece46153689a1eadea81e Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 18 Sep 2026 08:30:33 +0100 Subject: [PATCH 1/3] fix(client): route electrum & esplora traffic through config proxy `--proxy` accepted proxy_opts for both electrum and esplora clients but did not use them during connection. This fix updates both clients to use the provided proxy options during connection - added test for connection through proxy - updated CHANGELOG --- CHANGELOG.md | 2 + src/client.rs | 86 ++++++++++++++- tests/cli.rs | 1 + tests/integration/proxy.rs | 209 +++++++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 tests/integration/proxy.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d127b14..7f4cd38f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. - Fixed the data directory and config.toml permission being world-readable (0755/0644) to 0700/0600 on Unix. - Fixed `create_tx` and `bump_fee` panicking on malformed `--utxos` and `--add_data` values instead of returning an error - Fixed `--fee_rate` silently truncating to a whole sat/vB, falling back to a default, or producing a zero-fee transaction, unusable values are now rejected +- Fixed routing electrum and esplora traffic through configured socks5 proxy +- Rejected `--proxy` on the `rpc` and `cbf` backends ## [4.0.0] diff --git a/src/client.rs b/src/client.rs index 54c28d55..6d69898b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -22,6 +22,10 @@ use { bdk_wallet::chain::CanonicalizationParams, }; +#[cfg(any(feature = "electrum", feature = "esplora"))] +use crate::commands::ProxyOpts; +#[cfg(feature = "electrum")] +use std::time::Duration; #[cfg(feature = "cbf")] use {crate::utils::trace_logger, bdk_kyoto::BuilderExt}; @@ -202,6 +206,71 @@ pub struct KyotoClientHandle { tokio::sync::Mutex>, } +/// Build the electrum [`Config`] from the wallet's SOCKS5 proxy options. +/// +/// The hostname is handed to the proxy as a `TargetAddr::Domain`, so the target is +/// resolved by the proxy rather than locally, and no DNS query leaks. +#[cfg(feature = "electrum")] +fn electrum_config(proxy_opts: &ProxyOpts) -> bdk_electrum::electrum_client::Config { + use bdk_electrum::electrum_client::{ConfigBuilder, Socks5Config}; + + let socks5 = proxy_opts + .proxy + .as_ref() + .map(|addr| match &proxy_opts.proxy_auth { + Some((user, password)) => { + Socks5Config::with_credentials(addr, user.clone(), password.clone()) + } + None => Socks5Config::new(addr), + }); + + ConfigBuilder::new() + .socks5(socks5) + .retry(proxy_opts.retries) + .timeout( + proxy_opts + .timeout + .map(|secs| Duration::from_secs(secs as u64)), + ) + .build() +} + +/// Render the SOCKS5 proxy options as a URL for esplora's HTTP client. +/// +/// `socks5h` rather than `socks5` so the proxy resolves the esplora hostname; with +/// plain `socks5` the client resolves it locally first, leaking a DNS query that +/// identifies the server being synced against. +#[cfg(feature = "esplora")] +fn esplora_proxy_url(proxy_opts: &ProxyOpts) -> Option { + let addr = proxy_opts.proxy.as_ref()?; + let addr = addr + .strip_prefix("socks5h://") + .or_else(|| addr.strip_prefix("socks5://")) + .unwrap_or(addr); + + Some(match &proxy_opts.proxy_auth { + Some((user, password)) => format!("socks5h://{user}:{password}@{addr}"), + None => format!("socks5h://{addr}"), + }) +} + +/// Reject a proxy the backend client does not understand. +/// +/// `bitcoind`'s RPC client and the compact block filter backend have no SOCKS5 +/// support here, so a proxy set against them would silently do nothing. +#[cfg(all( + any(feature = "electrum", feature = "esplora"), + any(feature = "rpc", feature = "cbf") +))] +fn reject_unsupported_proxy(proxy_opts: &ProxyOpts, backend: &str) -> Result<(), Error> { + match proxy_opts.proxy { + Some(_) => Err(Error::Generic(format!( + "The {backend} backend does not support a SOCKS5 proxy. Remove --proxy, or use the electrum or esplora backend." + ))), + None => Ok(()), + } +} + #[cfg(any( feature = "electrum", feature = "esplora", @@ -219,7 +288,8 @@ pub(crate) fn new_blockchain_client( let client = match wallet_opts.client_type { #[cfg(feature = "electrum")] ClientType::Electrum => { - let client = bdk_electrum::electrum_client::Client::new(url) + let config = electrum_config(&wallet_opts.proxy_opts); + let client = bdk_electrum::electrum_client::Client::from_config(url, config) .map(bdk_electrum::BdkElectrumClient::new)?; BlockchainClient::Electrum { client: Box::new(client), @@ -228,7 +298,15 @@ pub(crate) fn new_blockchain_client( } #[cfg(feature = "esplora")] ClientType::Esplora => { - let client = bdk_esplora::esplora_client::Builder::new(url).build_async()?; + let mut builder = bdk_esplora::esplora_client::Builder::new(url) + .max_retries(wallet_opts.proxy_opts.retries as usize); + if let Some(proxy) = esplora_proxy_url(&wallet_opts.proxy_opts) { + builder = builder.proxy(&proxy); + } + if let Some(timeout) = wallet_opts.proxy_opts.timeout { + builder = builder.timeout(timeout as u64); + } + let client = builder.build_async()?; BlockchainClient::Esplora { client: Box::new(client), parallel_requests: wallet_opts.parallel_requests, @@ -237,6 +315,8 @@ pub(crate) fn new_blockchain_client( #[cfg(feature = "rpc")] ClientType::Rpc => { + #[cfg(any(feature = "electrum", feature = "esplora"))] + reject_unsupported_proxy(&wallet_opts.proxy_opts, "rpc")?; let auth = match &wallet_opts.cookie { Some(cookie) => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::CookieFile(cookie.into()), None => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::UserPass( @@ -253,6 +333,8 @@ pub(crate) fn new_blockchain_client( #[cfg(feature = "cbf")] ClientType::Cbf => { + #[cfg(any(feature = "electrum", feature = "esplora"))] + reject_unsupported_proxy(&wallet_opts.proxy_opts, "cbf")?; let scan_type = bdk_kyoto::ScanType::Sync; let builder = bdk_kyoto::builder::Builder::new(_wallet.network()); diff --git a/tests/cli.rs b/tests/cli.rs index 9285327c..c4759043 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -17,4 +17,5 @@ mod integration { mod init; mod offline; mod online; + mod proxy; } diff --git a/tests/integration/proxy.rs b/tests/integration/proxy.rs new file mode 100644 index 00000000..264cc272 --- /dev/null +++ b/tests/integration/proxy.rs @@ -0,0 +1,209 @@ +//! The SOCKS5 proxy options must actually reach the blockchain client. +//! +//! These tests stand two TCP listeners in for the chain server and the proxy, so +//! they need neither a real node nor a real SOCKS5 service: all that matters is +//! which port the connection arrives on. +#[cfg(any(feature = "electrum", feature = "esplora"))] +mod test_proxy { + use crate::common::BdkCli; + use assert_cmd::Command; + #[cfg(feature = "rpc")] + use predicates::prelude::*; + use serde_json::Value; + use std::net::{TcpListener, TcpStream}; + use std::sync::mpsc::{Receiver, channel}; + use std::thread; + use std::time::Duration; + use tempfile::TempDir; + + static WALLET_NAME: &str = "proxy_test_wallet"; + + fn spawn_listener() -> (String, Receiver<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind listener"); + let addr = listener.local_addr().unwrap().to_string(); + let (tx, rx) = channel(); + + thread::spawn(move || { + for stream in listener.incoming() { + match stream { + Ok(stream) => { + drop::(stream); + if tx.send(()).is_err() { + break; + } + } + Err(_) => break, + } + } + }); + + (addr, rx) + } + + fn connected(rx: &Receiver<()>) -> bool { + rx.recv_timeout(Duration::from_secs(5)).is_ok() + } + + /// As above, but for an arbitrary client type and url. + fn setup_wallet_for( + client_type: &str, + url: &str, + proxy_addr: Option<&str>, + ) -> (BdkCli, TempDir) { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + let desc = cli + .cmd("descriptor", &["--type", "wpkh"]) + .output() + .expect("failed to generate descriptors"); + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("invalid JSON from descriptor"); + let public = &desc_values["public_descriptors"]; + let ext_desc = public["external"].as_str().unwrap(); + let int_desc = public["internal"].as_str().unwrap(); + + let mut cmd = cli.build_base_cmd(); + cmd.arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg(client_type) + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg(url); + if let Some(proxy) = proxy_addr { + cmd.arg("--proxy").arg(proxy); + } + cmd.assert().success(); + + (cli, temp_dir) + } + + /// Runs `sync` and returns the command, so callers can assert on how it failed. + fn sync_cmd(cli: &BdkCli) -> Command { + let mut cmd = cli.wallet_cmd(&["--wallet", WALLET_NAME, "sync"]); + cmd.timeout(Duration::from_secs(30)); + cmd + } + + /// With `--proxy` set, the connection must go to the proxy and never to the + /// chain server directly. + #[cfg(feature = "electrum")] + #[test] + fn test_electrum_sync_goes_through_the_proxy() { + let (server_addr, server_rx) = spawn_listener(); + let (proxy_addr, proxy_rx) = spawn_listener(); + + let (cli, _temp_dir) = setup_wallet_for( + "electrum", + &format!("tcp://{server_addr}"), + Some(&proxy_addr), + ); + + // The stub proxy does not speak SOCKS5, so the sync must fail rather than + // quietly falling back to a direct connection. + sync_cmd(&cli).assert().failure(); + + assert!( + connected(&proxy_rx), + "the proxy was never contacted: traffic bypassed --proxy" + ); + assert!( + !connected(&server_rx), + "a direct connection reached the chain server despite --proxy" + ); + } + + /// Without `--proxy`, the connection goes straight to the chain server. This is + /// the control: it shows the test above is detecting the proxy, not a failure + /// to connect at all. + #[cfg(feature = "electrum")] + #[test] + fn test_electrum_sync_without_proxy_goes_direct() { + let (server_addr, server_rx) = spawn_listener(); + + let (cli, _temp_dir) = setup_wallet_for("electrum", &format!("tcp://{server_addr}"), None); + + sync_cmd(&cli).assert().failure(); + + assert!( + connected(&server_rx), + "no connection reached the chain server" + ); + } + + /// The esplora backend must honour `--proxy` just as electrum does. + #[cfg(feature = "esplora")] + #[test] + fn test_esplora_sync_goes_through_the_proxy() { + let (server_addr, server_rx) = spawn_listener(); + let (proxy_addr, proxy_rx) = spawn_listener(); + + let (cli, _temp_dir) = setup_wallet_for( + "esplora", + &format!("http://{server_addr}"), + Some(&proxy_addr), + ); + + sync_cmd(&cli).assert().failure(); + + assert!( + connected(&proxy_rx), + "the proxy was never contacted: traffic bypassed --proxy" + ); + assert!( + !connected(&server_rx), + "a direct connection reached the chain server despite --proxy" + ); + } + + /// A proxy the backend cannot honour is rejected, rather than ignored. + #[cfg(feature = "rpc")] + #[test] + fn test_rpc_backend_rejects_a_proxy() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + let desc = cli + .cmd("descriptor", &["--type", "wpkh"]) + .output() + .expect("failed to generate descriptors"); + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("invalid JSON from descriptor"); + let public = &desc_values["public_descriptors"]; + + cli.build_base_cmd() + .arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(public["external"].as_str().unwrap()) + .arg("--int-descriptor") + .arg(public["internal"].as_str().unwrap()) + .arg("--client-type") + .arg("rpc") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg("127.0.0.1:18443") + .arg("--proxy") + .arg("127.0.0.1:9050") + .assert() + .success(); + + sync_cmd(&cli) + .assert() + .failure() + .stderr(predicate::str::contains( + "rpc backend does not support a SOCKS5 proxy", + )); + } +} From 2b4866a79cd3230963a66a20ab454721b1bfefec Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 18 Sep 2026 20:58:01 +0100 Subject: [PATCH 2/3] fix(client): Route cbf traffic via configured SOCKS5 proxy - The cbf arm of new_blockchain_client was ignored proxy_opts but bdk_kyoto 0.17.0 support a proxy. This change adds the connection through socks5 proxy. - add tests coverage for cbf connection through socks5 proxy --- CHANGELOG.md | 4 +- src/client.rs | 29 ++----- src/commands.rs | 150 +++++++++++++++++++++++++++++++++- src/config.rs | 11 ++- src/handlers/config.rs | 2 +- tests/integration/proxy.rs | 162 +++++++++++++++++++++++++++++-------- 6 files changed, 294 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4cd38f..f2bf0503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details. - Fixed `create_tx` and `bump_fee` panicking on malformed `--utxos` and `--add_data` values instead of returning an error - Fixed `--fee_rate` silently truncating to a whole sat/vB, falling back to a default, or producing a zero-fee transaction, unusable values are now rejected - Fixed routing electrum and esplora traffic through configured socks5 proxy -- Rejected `--proxy` on the `rpc` and `cbf` backends - +- Routed compact filter (cbf) traffic through the configured SOCKS5 proxy +- Rejected `--proxy` on the `rpc` backend, and unsupported proxy options (`--proxy_auth`, `--timeout`) on the `rpc` and `cbf` backends, instead of silently ignoring them ## [4.0.0] diff --git a/src/client.rs b/src/client.rs index 6d69898b..bce66a18 100644 --- a/src/client.rs +++ b/src/client.rs @@ -254,23 +254,6 @@ fn esplora_proxy_url(proxy_opts: &ProxyOpts) -> Option { }) } -/// Reject a proxy the backend client does not understand. -/// -/// `bitcoind`'s RPC client and the compact block filter backend have no SOCKS5 -/// support here, so a proxy set against them would silently do nothing. -#[cfg(all( - any(feature = "electrum", feature = "esplora"), - any(feature = "rpc", feature = "cbf") -))] -fn reject_unsupported_proxy(proxy_opts: &ProxyOpts, backend: &str) -> Result<(), Error> { - match proxy_opts.proxy { - Some(_) => Err(Error::Generic(format!( - "The {backend} backend does not support a SOCKS5 proxy. Remove --proxy, or use the electrum or esplora backend." - ))), - None => Ok(()), - } -} - #[cfg(any( feature = "electrum", feature = "esplora", @@ -315,8 +298,7 @@ pub(crate) fn new_blockchain_client( #[cfg(feature = "rpc")] ClientType::Rpc => { - #[cfg(any(feature = "electrum", feature = "esplora"))] - reject_unsupported_proxy(&wallet_opts.proxy_opts, "rpc")?; + wallet_opts.reject_proxy("rpc")?; let auth = match &wallet_opts.cookie { Some(cookie) => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::CookieFile(cookie.into()), None => bdk_bitcoind_rpc::bitcoincore_rpc::Auth::UserPass( @@ -333,10 +315,13 @@ pub(crate) fn new_blockchain_client( #[cfg(feature = "cbf")] ClientType::Cbf => { - #[cfg(any(feature = "electrum", feature = "esplora"))] - reject_unsupported_proxy(&wallet_opts.proxy_opts, "cbf")?; + wallet_opts.reject_proxy_auth("cbf")?; + let scan_type = bdk_kyoto::ScanType::Sync; - let builder = bdk_kyoto::builder::Builder::new(_wallet.network()); + let mut builder = bdk_kyoto::builder::Builder::new(_wallet.network()); + if let Some(proxy) = wallet_opts.proxy_opts.socket_addr()? { + builder = builder.socks5_proxy(proxy); + } let light_client = builder .required_peers(wallet_opts.compactfilter_opts.conn_count) diff --git a/src/commands.rs b/src/commands.rs index 37f80523..86826ffe 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -53,9 +53,10 @@ use bdk_wallet::bitcoin::Network; use clap::{Args, Parser, Subcommand, value_parser}; use clap_complete::Shell; +#[cfg(any(feature = "rpc", feature = "cbf"))] +use crate::error::BDKCliError as Error; #[cfg(feature = "dns_payment")] use crate::handlers::dns::{CreateDnsTxCommand, ResolveDnsRecipientCommand}; - #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] use crate::utils::parse_proxy_auth; @@ -284,13 +285,68 @@ pub struct WalletOpts { #[cfg(feature = "cbf")] #[clap(flatten)] pub compactfilter_opts: CompactFilterOpts, - #[cfg(any(feature = "electrum", feature = "esplora"))] + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] #[command(flatten)] pub proxy_opts: ProxyOpts, } +#[cfg(any(feature = "rpc", feature = "cbf"))] +impl WalletOpts { + /// Reject a proxy the selected backend cannot honour at all, rather than + /// silently ignoring it. + /// + /// `--retries` cannot be checked the same way: it defaults to 5, so a + /// user-supplied value cannot be told apart from the default. + #[cfg(feature = "rpc")] + pub(crate) fn reject_proxy(&self, _backend: &str) -> Result<(), Error> { + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] + if self.proxy_opts.proxy.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support a SOCKS5 proxy. \ + Remove --proxy, or use the electrum, esplora or cbf backend." + ))); + } + #[cfg(any(feature = "electrum", feature = "esplora"))] + if self.proxy_opts.proxy_auth.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --proxy_auth." + ))); + } + #[cfg(any(feature = "electrum", feature = "esplora"))] + if self.proxy_opts.timeout.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --timeout." + ))); + } + Ok(()) + } + + /// Reject proxy options the cbf backend cannot honour, even though it does + /// support `--proxy` itself. + /// + /// Kyoto takes the proxy as a bare `SocketAddr`, so it has nowhere to put a + /// username and password, and no proxy-specific timeout knob. `--retries` + /// cannot be checked for the same reason noted on `reject_proxy`. + #[cfg(feature = "cbf")] + pub(crate) fn reject_proxy_auth(&self, _backend: &str) -> Result<(), Error> { + #[cfg(any(feature = "electrum", feature = "esplora"))] + if self.proxy_opts.proxy_auth.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --proxy_auth." + ))); + } + #[cfg(any(feature = "electrum", feature = "esplora"))] + if self.proxy_opts.timeout.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --timeout." + ))); + } + Ok(()) + } +} + /// Options to configure a SOCKS5 proxy for a blockchain client connection. -#[cfg(any(feature = "electrum", feature = "esplora"))] +#[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] #[derive(Debug, Args, Clone, PartialEq, Eq)] pub struct ProxyOpts { /// Sets the SOCKS5 proxy for a blockchain client. @@ -298,10 +354,12 @@ pub struct ProxyOpts { pub proxy: Option, /// Sets the SOCKS5 proxy credential. + #[cfg(any(feature = "electrum", feature = "esplora"))] #[arg(env = "PROXY_USER:PASSWD", long="proxy_auth", value_parser = parse_proxy_auth)] pub proxy_auth: Option<(String, String)>, /// Sets the SOCKS5 proxy retries for the blockchain client. + #[cfg(any(feature = "electrum", feature = "esplora"))] #[arg( env = "PROXY_RETRIES", short = 'r', @@ -311,10 +369,32 @@ pub struct ProxyOpts { pub retries: u8, /// Sets the SOCKS5 proxy timeout for the blockchain client. + #[cfg(any(feature = "electrum", feature = "esplora"))] #[arg(env = "PROXY_TIMEOUT", short = 't', long = "timeout")] pub timeout: Option, } +#[cfg(feature = "cbf")] +impl ProxyOpts { + /// The proxy as a [`SocketAddr`] for kyoto. + /// + /// Unlike the electrum and esplora backends this cannot take a hostname. + pub(crate) fn socket_addr(&self) -> Result, Error> { + let Some(addr) = self.proxy.as_ref() else { + return Ok(None); + }; + let addr = addr + .strip_prefix("socks5h://") + .or_else(|| addr.strip_prefix("socks5://")) + .unwrap_or(addr); + + addr.parse().map(Some).map_err(|_| { + Error::Generic(format!( + "The cbf backend needs --proxy as an ip:port address, but got '{addr}'." + )) + }) + } +} /// Options to configure a BIP157 Compact Filter backend. #[cfg(feature = "cbf")] #[derive(Debug, Args, Clone, PartialEq, Eq)] @@ -437,3 +517,67 @@ pub enum ReplSubCommand { /// Exit REPL loop. Exit, } + +#[cfg(all(test, feature = "cbf"))] +mod cbf_proxy_tests { + use super::*; + use std::net::SocketAddr; + + /// `ProxyOpts` carrying only a proxy; the other fields exist for the electrum + /// and esplora backends, which kyoto does not share. + fn proxy_opts(proxy: &str) -> ProxyOpts { + ProxyOpts { + proxy: Some(proxy.to_string()), + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_auth: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + retries: 5, + #[cfg(any(feature = "electrum", feature = "esplora"))] + timeout: None, + } + } + + #[test] + fn parses_the_spellings_the_other_backends_accept() { + let expected = Some(SocketAddr::from(([127, 0, 0, 1], 9050))); + + assert_eq!( + proxy_opts("127.0.0.1:9050").socket_addr().unwrap(), + expected + ); + assert_eq!( + proxy_opts("socks5://127.0.0.1:9050").socket_addr().unwrap(), + expected + ); + assert_eq!( + proxy_opts("socks5h://127.0.0.1:9050") + .socket_addr() + .unwrap(), + expected + ); + } + + #[test] + fn parses_an_ipv6_proxy() { + assert_eq!( + proxy_opts("[::1]:9050").socket_addr().unwrap(), + Some("[::1]:9050".parse::().unwrap()) + ); + } + + #[test] + fn rejects_a_hostname_kyoto_cannot_use() { + let err = proxy_opts("tor.local:9050").socket_addr().unwrap_err(); + assert!( + err.to_string().contains("ip:port"), + "unhelpful error: {err}" + ); + } + + #[test] + fn no_proxy_is_not_an_error() { + let mut opts = proxy_opts("127.0.0.1:9050"); + opts.proxy = None; + assert_eq!(opts.socket_addr().unwrap(), None); + } +} diff --git a/src/config.rs b/src/config.rs index e905c427..5e95dc5e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,7 +51,7 @@ pub struct WalletConfigInner { pub parallel_requests: Option, #[cfg(feature = "rpc")] pub cookie: Option, - #[cfg(any(feature = "electrum", feature = "esplora"))] + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] #[serde(default)] pub proxy: Option, #[cfg(any(feature = "electrum", feature = "esplora"))] @@ -175,14 +175,17 @@ impl TryFrom<&WalletConfigInner> for WalletOpts { #[cfg(feature = "rpc")] cookie: config.cookie.clone(), - #[cfg(any(feature = "electrum", feature = "esplora"))] + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] proxy_opts: crate::commands::ProxyOpts { proxy: config.proxy.clone(), + #[cfg(any(feature = "electrum", feature = "esplora"))] proxy_auth: match &config.proxy_auth { Some(s) => Some(crate::utils::parse_proxy_auth(s)?), None => None, }, + #[cfg(any(feature = "electrum", feature = "esplora"))] retries: config.proxy_retries.unwrap_or(5), + #[cfg(any(feature = "electrum", feature = "esplora"))] timeout: config.proxy_timeout, }, @@ -251,7 +254,7 @@ mod tests { rpc_password: None, #[cfg(feature = "rpc")] cookie: None, - #[cfg(any(feature = "electrum", feature = "esplora"))] + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] proxy: None, #[cfg(any(feature = "electrum", feature = "esplora"))] proxy_auth: None, @@ -336,7 +339,7 @@ mod tests { rpc_password: None, #[cfg(feature = "rpc")] cookie: None, - #[cfg(any(feature = "electrum", feature = "esplora"))] + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] proxy: None, #[cfg(any(feature = "electrum", feature = "esplora"))] proxy_auth: None, diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 13131c19..905011a6 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -128,7 +128,7 @@ impl AppCommand> for SaveConfigCommand { #[cfg(feature = "rpc")] cookie: self.wallet_opts.cookie.clone(), - #[cfg(any(feature = "electrum", feature = "esplora"))] + #[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] proxy: self.wallet_opts.proxy_opts.proxy.clone(), #[cfg(any(feature = "electrum", feature = "esplora"))] proxy_auth: self diff --git a/tests/integration/proxy.rs b/tests/integration/proxy.rs index 264cc272..2d676b6f 100644 --- a/tests/integration/proxy.rs +++ b/tests/integration/proxy.rs @@ -3,21 +3,27 @@ //! These tests stand two TCP listeners in for the chain server and the proxy, so //! they need neither a real node nor a real SOCKS5 service: all that matters is //! which port the connection arrives on. -#[cfg(any(feature = "electrum", feature = "esplora"))] +#[cfg(any(feature = "electrum", feature = "esplora", feature = "cbf"))] mod test_proxy { use crate::common::BdkCli; use assert_cmd::Command; - #[cfg(feature = "rpc")] + #[cfg(any(feature = "rpc", feature = "cbf"))] use predicates::prelude::*; use serde_json::Value; + #[cfg(any(feature = "electrum", feature = "esplora"))] use std::net::{TcpListener, TcpStream}; + #[cfg(any(feature = "electrum", feature = "esplora"))] use std::sync::mpsc::{Receiver, channel}; + #[cfg(any(feature = "electrum", feature = "esplora"))] use std::thread; use std::time::Duration; use tempfile::TempDir; static WALLET_NAME: &str = "proxy_test_wallet"; + #[cfg(any(feature = "electrum", feature = "esplora"))] + /// Accept connections on an ephemeral port, reporting each one and closing it + /// immediately. Returns the bound address and the receiving end of the report. fn spawn_listener() -> (String, Receiver<()>) { let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind listener"); let addr = listener.local_addr().unwrap().to_string(); @@ -40,11 +46,15 @@ mod test_proxy { (addr, rx) } + #[cfg(any(feature = "electrum", feature = "esplora"))] + /// Did a connection arrive within the grace period? fn connected(rx: &Receiver<()>) -> bool { rx.recv_timeout(Duration::from_secs(5)).is_ok() } - /// As above, but for an arbitrary client type and url. + /// Configure a wallet against `url`, optionally through `proxy_addr`, for the + /// backends that talk to a listener the tests can watch. + #[cfg(any(feature = "electrum", feature = "esplora"))] fn setup_wallet_for( client_type: &str, url: &str, @@ -93,6 +103,45 @@ mod test_proxy { cmd } + /// Configure a wallet for `client_type` with `extra_args` appended to the + /// `config` command (e.g. `--proxy`, `--proxy_auth`, `--timeout`). No listener + /// is needed: these tests only exercise validation, which happens before any + /// connection is attempted, at `sync` time. + #[cfg(any(feature = "rpc", feature = "cbf"))] + fn setup_wallet_with_args(client_type: &str, extra_args: &[&str]) -> (BdkCli, TempDir) { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + let desc = cli + .cmd("descriptor", &["--type", "wpkh"]) + .output() + .expect("failed to generate descriptors"); + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("invalid JSON from descriptor"); + let public = &desc_values["public_descriptors"]; + + let mut cmd = cli.build_base_cmd(); + cmd.arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(public["external"].as_str().unwrap()) + .arg("--int-descriptor") + .arg(public["internal"].as_str().unwrap()) + .arg("--client-type") + .arg(client_type) + .arg("--database-type") + .arg("sqlite"); + // `--url` is required whenever electrum, esplora or rpc is built, no matter + // which `--client-type` is chosen; it does not exist at all otherwise. + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + cmd.arg("--url").arg("127.0.0.1:18443"); + cmd.args(extra_args).assert().success(); + + (cli, temp_dir) + } + /// With `--proxy` set, the connection must go to the proxy and never to the /// chain server directly. #[cfg(feature = "electrum")] @@ -164,46 +213,95 @@ mod test_proxy { ); } - /// A proxy the backend cannot honour is rejected, rather than ignored. + /// A proxy the backend cannot honour at all is rejected, rather than ignored. #[cfg(feature = "rpc")] #[test] fn test_rpc_backend_rejects_a_proxy() { - let temp_dir = TempDir::new().unwrap(); - let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + let (cli, _temp_dir) = setup_wallet_with_args("rpc", &["--proxy", "127.0.0.1:9050"]); - let desc = cli - .cmd("descriptor", &["--type", "wpkh"]) - .output() - .expect("failed to generate descriptors"); - let desc_values: Value = - serde_json::from_slice(&desc.stdout).expect("invalid JSON from descriptor"); - let public = &desc_values["public_descriptors"]; + sync_cmd(&cli) + .assert() + .failure() + .stderr(predicate::str::contains( + "rpc backend does not support a SOCKS5 proxy", + )); + } - cli.build_base_cmd() - .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) - .arg("config") - .arg("--ext-descriptor") - .arg(public["external"].as_str().unwrap()) - .arg("--int-descriptor") - .arg(public["internal"].as_str().unwrap()) - .arg("--client-type") - .arg("rpc") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg("127.0.0.1:18443") - .arg("--proxy") - .arg("127.0.0.1:9050") + /// `--proxy_auth` alone (no `--proxy`) is also rejected for rpc, not just + /// silently dropped. + #[cfg(all(feature = "rpc", any(feature = "electrum", feature = "esplora")))] + #[test] + fn test_rpc_backend_rejects_proxy_auth() { + let (cli, _temp_dir) = setup_wallet_with_args("rpc", &["--proxy_auth", "user:password"]); + + sync_cmd(&cli) .assert() - .success(); + .failure() + .stderr(predicate::str::contains( + "rpc backend does not support --proxy_auth", + )); + } + + /// `--timeout` alone is likewise rejected for rpc. + #[cfg(all(feature = "rpc", any(feature = "electrum", feature = "esplora")))] + #[test] + fn test_rpc_backend_rejects_timeout() { + let (cli, _temp_dir) = setup_wallet_with_args("rpc", &["--timeout", "30"]); sync_cmd(&cli) .assert() .failure() .stderr(predicate::str::contains( - "rpc backend does not support a SOCKS5 proxy", + "rpc backend does not support --timeout", + )); + } + + /// Kyoto takes the proxy as a `SocketAddr`, so a hostname is reported rather + /// than accepted and then failing obscurely. + #[cfg(feature = "cbf")] + #[test] + fn test_cbf_backend_rejects_a_proxy_hostname() { + let (cli, _temp_dir) = setup_wallet_with_args("cbf", &["--proxy", "tor.local:9050"]); + + sync_cmd(&cli) + .assert() + .failure() + .stderr(predicate::str::contains( + "cbf backend needs --proxy as an ip:port address", + )); + } + + /// Kyoto's proxy carries no credentials, so `--proxy_auth` is reported rather + /// than silently dropped. + #[cfg(all(feature = "cbf", any(feature = "electrum", feature = "esplora")))] + #[test] + fn test_cbf_backend_rejects_proxy_auth() { + let (cli, _temp_dir) = setup_wallet_with_args( + "cbf", + &["--proxy", "127.0.0.1:9050", "--proxy_auth", "user:password"], + ); + + sync_cmd(&cli) + .assert() + .failure() + .stderr(predicate::str::contains( + "cbf backend does not support --proxy_auth", + )); + } + + /// Kyoto has no proxy-specific timeout knob, so `--timeout` is reported rather + /// than silently dropped. + #[cfg(all(feature = "cbf", any(feature = "electrum", feature = "esplora")))] + #[test] + fn test_cbf_backend_rejects_timeout() { + let (cli, _temp_dir) = + setup_wallet_with_args("cbf", &["--proxy", "127.0.0.1:9050", "--timeout", "30"]); + + sync_cmd(&cli) + .assert() + .failure() + .stderr(predicate::str::contains( + "cbf backend does not support --timeout", )); } } From e8fc1e3ed10e351886b31cdd431fc4bda974aca0 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 2 Oct 2026 06:14:16 +0100 Subject: [PATCH 3/3] fix(cli): use valid envt variable names for env fallbacks Several `#[arg(env = "...")]` attributes mistakenly used colons (e.g., `ADDRESS:SAT`) following value-shape documentation conventions. Because colons are invalid in POSIX environment variable names, these fallbacks could never actually be set by users. Renamed these environment variables to use valid identifiers while keeping the flag names and expected value shapes unchanged. Also, format integration tests --- src/commands.rs | 48 +++++++++++++++++++----------------- src/handlers/dns/mod.rs | 6 ++--- src/handlers/offline.rs | 16 ++++++------ tests/common/mod.rs | 4 +-- tests/integration/init.rs | 48 ++++++++++++------------------------ tests/integration/offline.rs | 27 +++++++------------- tests/integration/online.rs | 36 +++++++++------------------ tests/integration/proxy.rs | 39 +++++++++++------------------ 8 files changed, 90 insertions(+), 134 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 86826ffe..d921e7ce 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -271,7 +271,7 @@ pub struct WalletOpts { #[cfg(feature = "rpc")] /// Sets the rpc basic authentication. #[arg( - env = "USER:PASSWD", + env = "RPC_BASIC_AUTH", short = 'a', long, value_parser = parse_proxy_auth, @@ -307,16 +307,17 @@ impl WalletOpts { ))); } #[cfg(any(feature = "electrum", feature = "esplora"))] - if self.proxy_opts.proxy_auth.is_some() { - return Err(Error::Generic(format!( - "The {_backend} backend does not support --proxy_auth." - ))); - } - #[cfg(any(feature = "electrum", feature = "esplora"))] - if self.proxy_opts.timeout.is_some() { - return Err(Error::Generic(format!( - "The {_backend} backend does not support --timeout." - ))); + { + if self.proxy_opts.proxy_auth.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --proxy_auth." + ))); + } + if self.proxy_opts.timeout.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --timeout." + ))); + } } Ok(()) } @@ -330,16 +331,17 @@ impl WalletOpts { #[cfg(feature = "cbf")] pub(crate) fn reject_proxy_auth(&self, _backend: &str) -> Result<(), Error> { #[cfg(any(feature = "electrum", feature = "esplora"))] - if self.proxy_opts.proxy_auth.is_some() { - return Err(Error::Generic(format!( - "The {_backend} backend does not support --proxy_auth." - ))); - } - #[cfg(any(feature = "electrum", feature = "esplora"))] - if self.proxy_opts.timeout.is_some() { - return Err(Error::Generic(format!( - "The {_backend} backend does not support --timeout." - ))); + { + if self.proxy_opts.proxy_auth.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --proxy_auth." + ))); + } + if self.proxy_opts.timeout.is_some() { + return Err(Error::Generic(format!( + "The {_backend} backend does not support --timeout." + ))); + } } Ok(()) } @@ -350,12 +352,12 @@ impl WalletOpts { #[derive(Debug, Args, Clone, PartialEq, Eq)] pub struct ProxyOpts { /// Sets the SOCKS5 proxy for a blockchain client. - #[arg(env = "PROXY_ADDRS:PORT", long = "proxy")] + #[arg(env = "PROXY_ADDRS_PORT", long = "proxy")] pub proxy: Option, /// Sets the SOCKS5 proxy credential. #[cfg(any(feature = "electrum", feature = "esplora"))] - #[arg(env = "PROXY_USER:PASSWD", long="proxy_auth", value_parser = parse_proxy_auth)] + #[arg(env = "PROXY_USER_PASSWD", long="proxy_auth", value_parser = parse_proxy_auth)] pub proxy_auth: Option<(String, String)>, /// Sets the SOCKS5 proxy retries for the blockchain client. diff --git a/src/handlers/dns/mod.rs b/src/handlers/dns/mod.rs index a5733690..645672c1 100644 --- a/src/handlers/dns/mod.rs +++ b/src/handlers/dns/mod.rs @@ -41,7 +41,7 @@ impl AsyncAppCommand> for ResolveDnsRecipientCommand { /// Creates a new unsigned transaction from DNS payment instructions. #[derive(Parser, Debug, Clone, PartialEq)] pub struct CreateDnsTxCommand { - #[arg(env = "ADDRESS:SAT", long = "to", value_parser = parse_recipient)] + #[arg(env = "ADDRESS_SAT", long = "to", value_parser = parse_recipient)] pub recipients: Vec<(ScriptBuf, u64)>, #[arg(long = "to_dns", value_parser = parse_dns_recipient)] pub dns_recipients: Vec<(String, u64)>, @@ -53,9 +53,9 @@ pub struct CreateDnsTxCommand { pub enable_rbf: bool, #[arg(long = "offline_signer")] pub offline_signer: bool, - #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + #[arg(env = "MUST_SPEND_TXID_VOUT", long = "utxos", value_parser = parse_outpoint)] pub utxos: Option>, - #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + #[arg(env = "CANT_SPEND_TXID_VOUT", long = "unspendable", value_parser = parse_outpoint)] pub unspendable: Option>, #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate", value_parser = parse_fee_rate)] pub fee_rate: Option, diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 4b623b49..15cdd26b 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -193,7 +193,7 @@ impl AppCommand>> for BalanceCommand { #[derive(Debug, Parser, Clone, PartialEq)] pub struct CreateTxCommand { /// Adds a recipient to the transaction. - #[arg(env = "ADDRESS:SAT", long = "to", required = true, value_parser = parse_recipient)] + #[arg(env = "ADDRESS_SAT", long = "to", required = true, value_parser = parse_recipient)] pub recipients: Vec<(ScriptBuf, u64)>, /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0. @@ -209,11 +209,11 @@ pub struct CreateTxCommand { pub offline_signer: bool, /// Selects which utxos *must* be spent. - #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + #[arg(env = "MUST_SPEND_TXID_VOUT", long = "utxos", value_parser = parse_outpoint)] pub utxos: Option>, /// Marks a utxo as unspendable. - #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + #[arg(env = "CANT_SPEND_TXID_VOUT", long = "unspendable", value_parser = parse_outpoint)] pub unspendable: Option>, /// Fee rate to use in sat/vbyte. @@ -327,7 +327,7 @@ pub struct CreateSpTxCommand { /// Adds a recipient to the transaction. // Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704. // Address and amount parsing is done at run time in handler function. - #[arg(env = "ADDRESS:SAT", long = "to", required = false, value_parser = parse_recipient)] + #[arg(env = "ADDRESS_SAT", long = "to", required = false, value_parser = parse_recipient)] pub recipients: Option>, /// Parse silent payment recipients #[arg(long = "to-sp", required = true, value_parser = parse_sp_code_value_pairs)] @@ -339,10 +339,10 @@ pub struct CreateSpTxCommand { #[arg(long = "offline_signer")] pub offline_signer: bool, /// Selects which utxos *must* be spent. - #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + #[arg(env = "MUST_SPEND_TXID_VOUT", long = "utxos", value_parser = parse_outpoint)] pub utxos: Option>, /// Marks a utxo as unspendable. - #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + #[arg(env = "CANT_SPEND_TXID_VOUT", long = "unspendable", value_parser = parse_outpoint)] pub unspendable: Option>, /// Fee rate to use in sat/vbyte. #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate", value_parser = parse_fee_rate)] @@ -547,11 +547,11 @@ pub struct BumpFeeCommand { pub offline_signer: bool, /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used. - #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + #[arg(env = "MUST_SPEND_TXID_VOUT", long = "utxos", value_parser = parse_outpoint)] pub utxos: Option>, /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees. - #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + #[arg(env = "CANT_SPEND_TXID_VOUT", long = "unspendable", value_parser = parse_outpoint)] pub unspendable: Option>, /// The new targeted fee rate in sat/vbyte. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 43220166..d38edf8b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -40,14 +40,14 @@ impl BdkCli { pub fn build_base_cmd(&self) -> Command { let mut cmd = Command::cargo_bin("bdk-cli").expect("bdk-cli binary must compile"); - cmd.arg("--network").arg(&self.network); + cmd.args(["--network", &self.network]); if let Some(dir) = &self.datadir { cmd.arg("--datadir").arg(dir); } if let Some(url) = &self.server_url { - cmd.arg("--server").arg(url); + cmd.args(["--server", url]); } cmd diff --git a/tests/integration/init.rs b/tests/integration/init.rs index 0a2af96f..4f57b460 100644 --- a/tests/integration/init.rs +++ b/tests/integration/init.rs @@ -138,19 +138,13 @@ mod test_wallets { cli.build_base_cmd() .arg("wallet") - .arg("--wallet") - .arg(wallet_name) + .args(["--wallet", wallet_name]) .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg("rpc") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg("http://localhost:18443") + .args(["--ext-descriptor", ext_desc]) + .args(["--int-descriptor", int_desc]) + .args(["--client-type", "rpc"]) + .args(["--database-type", "sqlite"]) + .args(["--url", "http://localhost:18443"]) .assert() .success(); } @@ -246,19 +240,13 @@ mod test_config { let mut cmd_init = cli.build_base_cmd(); cmd_init .arg("wallet") - .arg("--wallet") - .arg(wallet_name) + .args(["--wallet", wallet_name]) .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg(client_type) - .arg("--database-type") - .arg(db) - .arg("--url") - .arg(url); + .args(["--ext-descriptor", ext_desc]) + .args(["--int-descriptor", int_desc]) + .args(["--client-type", client_type]) + .args(["--database-type", db]) + .args(["--url", url]); cmd_init.assert().success(); @@ -310,19 +298,15 @@ mod test_config { cli.build_base_cmd() .arg("wallet") - .arg("--wallet") - .arg("secret_wallet") + .args(["--wallet", "secret_wallet"]) .arg("config") .arg("--ext-descriptor") .arg(priv_desc["external"].as_str().unwrap()) .arg("--int-descriptor") .arg(priv_desc["internal"].as_str().unwrap()) - .arg("--client-type") - .arg("rpc") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg("http://localhost:18443") + .args(["--client-type", "rpc"]) + .args(["--database-type", "sqlite"]) + .args(["--url", "http://localhost:18443"]) .assert() .success() .stderr(predicate::str::contains("PRIVATE KEYS")); diff --git a/tests/integration/offline.rs b/tests/integration/offline.rs index 4b678eba..417bf72b 100644 --- a/tests/integration/offline.rs +++ b/tests/integration/offline.rs @@ -35,19 +35,13 @@ mod test_offline { let mut cmd_init = cli.build_base_cmd(); cmd_init .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) + .args(["--wallet", WALLET_NAME]) .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg(client_type) - .arg("--database-type") - .arg(db) - .arg("--url") - .arg(url); + .args(["--ext-descriptor", ext_desc]) + .args(["--int-descriptor", int_desc]) + .args(["--client-type", client_type]) + .args(["--database-type", db]) + .args(["--url", url]); (cli, cmd_init) } @@ -438,13 +432,10 @@ mod multipath_tests { fn save_config(cli: &BdkCli, wallet: &str, ext: &str, int: Option<&str>) -> Command { let mut cmd = cli.build_base_cmd(); cmd.arg("wallet") - .arg("--wallet") - .arg(wallet) + .args(["--wallet", wallet]) .arg("config") - .arg("--ext-descriptor") - .arg(ext) - .arg("--database-type") - .arg("sqlite"); + .args(["--ext-descriptor", ext]) + .args(["--database-type", "sqlite"]); if let Some(int) = int { cmd.arg("--int-descriptor").arg(int); } diff --git a/tests/integration/online.rs b/tests/integration/online.rs index b9dcc7b4..4d3f11af 100644 --- a/tests/integration/online.rs +++ b/tests/integration/online.rs @@ -33,19 +33,13 @@ mod test_online { let mut cmd_init = cli.build_base_cmd(); cmd_init .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) + .args(["--wallet", WALLET_NAME]) .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg("electrum") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg(server_url); + .args(["--ext-descriptor", ext_desc]) + .args(["--int-descriptor", int_desc]) + .args(["--client-type", "electrum"]) + .args(["--database-type", "sqlite"]) + .args(["--url", server_url]); (cli, cmd_init, env) } @@ -862,19 +856,13 @@ mod test_online { .unwrap(); cli.build_base_cmd() .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) + .args(["--wallet", WALLET_NAME]) .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg("electrum") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg(server_url) + .args(["--ext-descriptor", ext_desc]) + .args(["--int-descriptor", int_desc]) + .args(["--client-type", "electrum"]) + .args(["--database-type", "sqlite"]) + .args(["--url", server_url]) .assert() .success(); diff --git a/tests/integration/proxy.rs b/tests/integration/proxy.rs index 2d676b6f..9b369468 100644 --- a/tests/integration/proxy.rs +++ b/tests/integration/proxy.rs @@ -10,14 +10,14 @@ mod test_proxy { #[cfg(any(feature = "rpc", feature = "cbf"))] use predicates::prelude::*; use serde_json::Value; - #[cfg(any(feature = "electrum", feature = "esplora"))] - use std::net::{TcpListener, TcpStream}; - #[cfg(any(feature = "electrum", feature = "esplora"))] - use std::sync::mpsc::{Receiver, channel}; - #[cfg(any(feature = "electrum", feature = "esplora"))] - use std::thread; use std::time::Duration; use tempfile::TempDir; + #[cfg(any(feature = "electrum", feature = "esplora"))] + use { + std::net::{TcpListener, TcpStream}, + std::sync::mpsc::{Receiver, channel}, + std::thread, + }; static WALLET_NAME: &str = "proxy_test_wallet"; @@ -75,19 +75,13 @@ mod test_proxy { let mut cmd = cli.build_base_cmd(); cmd.arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) + .args(["--wallet", WALLET_NAME]) .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg(client_type) - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg(url); + .args(["--ext-descriptor", ext_desc]) + .args(["--int-descriptor", int_desc]) + .args(["--client-type", client_type]) + .args(["--database-type", "sqlite"]) + .args(["--url", url]); if let Some(proxy) = proxy_addr { cmd.arg("--proxy").arg(proxy); } @@ -122,17 +116,14 @@ mod test_proxy { let mut cmd = cli.build_base_cmd(); cmd.arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) + .args(["--wallet", WALLET_NAME]) .arg("config") .arg("--ext-descriptor") .arg(public["external"].as_str().unwrap()) .arg("--int-descriptor") .arg(public["internal"].as_str().unwrap()) - .arg("--client-type") - .arg(client_type) - .arg("--database-type") - .arg("sqlite"); + .args(["--client-type", client_type]) + .args(["--database-type", "sqlite"]); // `--url` is required whenever electrum, esplora or rpc is built, no matter // which `--client-type` is chosen; it does not exist at all otherwise. #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))]