Summary
tests/electrum.rs depends on the electrumd dev-dependency, which downloads and runs the Electrum Python wallet as a subprocess to drive the in-process electrs Electrum RPC server end-to-end. This binary download is gated to Linux x86_64 and does not build on macOS (especially Apple Silicon) without an out-of-tree patch.
This proposes dropping electrumd and replacing the wallet subprocess with BDK (bdk_wallet + bdk_electrum) — a pure-Rust, descriptor-based HD wallet that syncs against an Electrum server. This keeps a genuine independent-wallet end-to-end signal while compiling on every platform with no downloaded binary and no build.rs platform gate.
Background / problem
electrumd's build.rs resolves its download artifact via a download_filename() that is #[cfg(all(target_os = "linux", target_arch = "x86_64"))]-gated. On any other host this is unresolved and produces a hard compile error in the dependency, which blocks the entire test harness — including --lib unit tests, not just the Electrum tests.
The current workaround repoints [patch.crates-io.electrumd] at a community fork that adds a macOS path, then reverts it before committing. That fork still pulls upstream Electrum's x86_64 artifacts (run under Rosetta on Apple Silicon) — upstream Electrum ships no native headless arm64 build — so it is brittle and expected to keep breaking on Apple Silicon.
Proposed approach: BDK as the test wallet
The electrs Electrum server is already started in-process by the test harness (common::init_electrum_tester() → ElectrumRPC::start); electrumd only supplies the client wallet pointed at it. Replace that wallet subprocess with BDK in-process.
BDK does the full wallet job against the server — descriptor-based HD derivation, full_scan/sync (driving batched script_get_history + transaction_get + block-header fetches), UTXO/balance computation, coin selection, PSBT signing, and broadcast — so the end-to-end "real wallet against electrs" signal is preserved. It is arguably a stronger signal than the Electrum Python wallet, because BDK is the dominant real-world consumer of electrs/esplora today.
Version alignment
| Crate |
Version |
Depends on |
bdk_electrum |
0.24.0 |
electrum-client ^0.25, bdk_core ^0.6.1 |
bdk_wallet |
1.1.0 |
bitcoin 0.32.6 |
| electrs |
— |
bitcoin 0.32.4 |
Same bitcoin major (0.32) end to end → shared Script / Transaction / Txid, no glue conversions. All pure Rust.
Cargo.toml
[dev-dependencies]
# remove:
# electrumd = { version = "0.1.0", features = ["4_6_2"] }
bdk_wallet = "1.1"
bdk_electrum = "0.24"
# remove the entire patch section:
# [patch.crates-io.electrumd]
# git = "https://github.com/shesek/electrumd"
Also remove the now-unused electrumd Error variant and From<electrumd::Error> impl in tests/common.rs.
Sketch
use bdk_wallet::Wallet;
use bdk_electrum::{electrum_client, BdkElectrumClient};
let (_srv, addr, mut t) = common::init_electrum_tester()?;
let mut wallet = Wallet::create(external_desc, internal_desc)
.network(Network::Regtest)
.create_wallet_no_persist()?;
let cli = BdkElectrumClient::new(electrum_client::Client::new(&format!("tcp://{addr}"))?);
// fund a BDK-derived address via bitcoind, mine, then sync:
let wallet_addr = wallet.next_unused_address(KeychainKind::External);
t.send(&wallet_addr.address, "0.1 BTC".parse()?)?;
t.mine()?;
let update = cli.full_scan(wallet.start_full_scan(), 5, 1, false)?;
wallet.apply_update(update)?;
assert_eq!(wallet.balance().confirmed.to_sat(), 10_000_000);
// spend: build + sign in BDK, broadcast through electrs:
let mut psbt = wallet.build_tx().add_recipient(dest_spk, Amount::from_sat(amount)).finish()?;
wallet.sign(&mut psbt, Default::default())?;
cli.transaction_broadcast(&psbt.extract_tx()?)?;
The existing tests map over cleanly: test_electrum_balance → fund + full_scan + wallet.balance(); test_electrum_history → wallet.transactions() after sync; test_electrum_payment → build/sign PSBT + transaction_broadcast.
One coverage gap to handle
BDK syncs by polling (full_scan/sync), not subscriptions. Unlike the old Electrum Python wallet, it won't exercise blockchain.headers.subscribe / scripthash subscription + the server's notify() push path that the current notify_wallet() flow tests. Keep that covered by either:
- Keeping
test_electrum_raw (already raw TCP) and adding a small blockchain.headers.subscribe / scripthash-subscribe smoke test over the socket, or
- Using
electrum-client's subscribe methods for that one assertion.
Net coverage still increases versus today.
Fallback approach: raw electrum-client
If the heavier BDK dependency tree is unwanted, the lighter alternative is the pure-Rust electrum-client (0.25, also bitcoin ^0.32) used as a thin protocol client, with bitcoind (already available via TestRunner::node_client()) acting as the wallet. Assertions become direct protocol calls — script_get_balance (sats), script_get_history, script_list_unspent, transaction_broadcast. This is simpler and minimal-dep, but drops the independent-wallet logic from the loop. Much of that protocol-level surface is already exercised by tests/rest.rs and the raw-socket test_electrum_raw.
Approaches considered
| Approach |
End-to-end wallet signal |
Cross-platform / pure Rust |
Deps |
Notes |
BDK (bdk_wallet + bdk_electrum) — proposed |
✅ strong (real wallet) |
✅ |
heavier |
Most representative consumer of electrs; no subscription coverage (handle separately) |
raw electrum-client + bitcoind |
⚠️ protocol-level only |
✅ |
minimal |
Simplest; overlaps tests/rest.rs / test_electrum_raw |
Keep electrumd + macOS fork patch |
✅ |
❌ (x86_64 only) |
— |
Status quo; breaks on Apple Silicon |
Fork electrumd to ship multi-arch binaries |
✅ |
⚠️ |
— |
Upstream Electrum has no headless arm64 build → most work, stays brittle |
Notes
- These tests are already Bitcoin-only (
#[cfg_attr(feature = "liquid", allow(dead_code))]), so the Liquid/confidential case does not need solving here.
- Removing
electrumd also eliminates the patch-revert ritual and the subprocess + wait_for_sync polling, making the suite faster.
Acceptance criteria
Summary
tests/electrum.rsdepends on theelectrumddev-dependency, which downloads and runs the Electrum Python wallet as a subprocess to drive the in-process electrs Electrum RPC server end-to-end. This binary download is gated to Linux x86_64 and does not build on macOS (especially Apple Silicon) without an out-of-tree patch.This proposes dropping
electrumdand replacing the wallet subprocess with BDK (bdk_wallet+bdk_electrum) — a pure-Rust, descriptor-based HD wallet that syncs against an Electrum server. This keeps a genuine independent-wallet end-to-end signal while compiling on every platform with no downloaded binary and nobuild.rsplatform gate.Background / problem
electrumd'sbuild.rsresolves its download artifact via adownload_filename()that is#[cfg(all(target_os = "linux", target_arch = "x86_64"))]-gated. On any other host this is unresolved and produces a hard compile error in the dependency, which blocks the entire test harness — including--libunit tests, not just the Electrum tests.The current workaround repoints
[patch.crates-io.electrumd]at a community fork that adds a macOS path, then reverts it before committing. That fork still pulls upstream Electrum's x86_64 artifacts (run under Rosetta on Apple Silicon) — upstream Electrum ships no native headless arm64 build — so it is brittle and expected to keep breaking on Apple Silicon.Proposed approach: BDK as the test wallet
The electrs Electrum server is already started in-process by the test harness (
common::init_electrum_tester()→ElectrumRPC::start);electrumdonly supplies the client wallet pointed at it. Replace that wallet subprocess with BDK in-process.BDK does the full wallet job against the server — descriptor-based HD derivation,
full_scan/sync(driving batchedscript_get_history+transaction_get+ block-header fetches), UTXO/balance computation, coin selection, PSBT signing, and broadcast — so the end-to-end "real wallet against electrs" signal is preserved. It is arguably a stronger signal than the Electrum Python wallet, because BDK is the dominant real-world consumer of electrs/esplora today.Version alignment
bdk_electrumelectrum-client ^0.25,bdk_core ^0.6.1bdk_walletbitcoin 0.32.6bitcoin 0.32.4Same
bitcoinmajor (0.32) end to end → sharedScript/Transaction/Txid, no glue conversions. All pure Rust.Cargo.toml
Also remove the now-unused
electrumdErrorvariant andFrom<electrumd::Error>impl intests/common.rs.Sketch
The existing tests map over cleanly:
test_electrum_balance→ fund +full_scan+wallet.balance();test_electrum_history→wallet.transactions()after sync;test_electrum_payment→ build/sign PSBT +transaction_broadcast.One coverage gap to handle
BDK syncs by polling (
full_scan/sync), not subscriptions. Unlike the old Electrum Python wallet, it won't exerciseblockchain.headers.subscribe/ scripthash subscription + the server'snotify()push path that the currentnotify_wallet()flow tests. Keep that covered by either:test_electrum_raw(already raw TCP) and adding a smallblockchain.headers.subscribe/ scripthash-subscribe smoke test over the socket, orelectrum-client's subscribe methods for that one assertion.Net coverage still increases versus today.
Fallback approach: raw
electrum-clientIf the heavier BDK dependency tree is unwanted, the lighter alternative is the pure-Rust
electrum-client(0.25, alsobitcoin ^0.32) used as a thin protocol client, with bitcoind (already available viaTestRunner::node_client()) acting as the wallet. Assertions become direct protocol calls —script_get_balance(sats),script_get_history,script_list_unspent,transaction_broadcast. This is simpler and minimal-dep, but drops the independent-wallet logic from the loop. Much of that protocol-level surface is already exercised bytests/rest.rsand the raw-sockettest_electrum_raw.Approaches considered
bdk_wallet+bdk_electrum) — proposedelectrum-client+ bitcoindtests/rest.rs/test_electrum_rawelectrumd+ macOS fork patchelectrumdto ship multi-arch binariesNotes
#[cfg_attr(feature = "liquid", allow(dead_code))]), so the Liquid/confidential case does not need solving here.electrumdalso eliminates the patch-revert ritual and the subprocess +wait_for_syncpolling, making the suite faster.Acceptance criteria
electrumdremoved fromCargo.toml(dependency +[patch.crates-io.electrumd]);bdk_wallet+bdk_electrumadded.electrumdErrorvariant /Fromimpl removed fromtests/common.rs.tests/electrum.rsrewritten against BDK.headers.subscribepath retained via a raw-socket test.cargo test --test electrumpasses on both Linux and Apple Silicon macOS.