Skip to content

Commit e017b0c

Browse files
committed
Add optional esplora wallet setup for payjoin-cli backend
This adds a new esplora feature to use a bdk wallet as the supporting backend for payjoin-cli to reduce the need for a bitcoind rpc connection when testing against integrations. This does retain the bitcoind functionality so we can continue to run local e2e tests during the payjoin-cli CI tests.
1 parent 637f913 commit e017b0c

File tree

15 files changed

+1516
-1101
lines changed

15 files changed

+1516
-1101
lines changed

Cargo-minimal.lock

Lines changed: 808 additions & 882 deletions
Large diffs are not rendered by default.

Cargo-recent.lock

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,11 @@ dependencies = [
434434
"bitcoin 0.30.2",
435435
"core-rpc",
436436
"electrum-client",
437-
"esplora-client",
437+
"esplora-client 0.6.0",
438438
"getrandom 0.2.15",
439439
"js-sys",
440440
"log",
441-
"miniscript",
441+
"miniscript 10.2.3",
442442
"rand 0.8.5",
443443
"serde",
444444
"serde_json",
@@ -457,6 +457,55 @@ dependencies = [
457457
"syn 1.0.109",
458458
]
459459

460+
[[package]]
461+
name = "bdk_chain"
462+
version = "0.23.3"
463+
source = "registry+https://github.com/rust-lang/crates.io-index"
464+
checksum = "c290eff038799a8ac0c5a82b6160a9ca456baa299a6f22b262c771342d2846c0"
465+
dependencies = [
466+
"bdk_core",
467+
"bitcoin 0.32.8",
468+
"miniscript 12.3.5",
469+
"serde",
470+
]
471+
472+
[[package]]
473+
name = "bdk_core"
474+
version = "0.6.3"
475+
source = "registry+https://github.com/rust-lang/crates.io-index"
476+
checksum = "cb3028782f6bf14a6df987244333d34e6b272b5a40a53e4879ec2dfd82275a3a"
477+
dependencies = [
478+
"bitcoin 0.32.8",
479+
"hashbrown 0.14.5",
480+
"serde",
481+
]
482+
483+
[[package]]
484+
name = "bdk_esplora"
485+
version = "0.22.2"
486+
source = "registry+https://github.com/rust-lang/crates.io-index"
487+
checksum = "83986307ea92997c3d051e8c306af8115a05add601e22acb7c1903008e6b614e"
488+
dependencies = [
489+
"async-trait",
490+
"bdk_core",
491+
"esplora-client 0.12.3",
492+
"futures",
493+
]
494+
495+
[[package]]
496+
name = "bdk_wallet"
497+
version = "3.0.0-rc.2"
498+
source = "registry+https://github.com/rust-lang/crates.io-index"
499+
checksum = "9b989a12f0398a844bf22b14a197e2676aea2775aabca52057627f596ad3a0ef"
500+
dependencies = [
501+
"bdk_chain",
502+
"bitcoin 0.32.8",
503+
"miniscript 12.3.5",
504+
"rand_core 0.6.4",
505+
"serde",
506+
"serde_json",
507+
]
508+
460509
[[package]]
461510
name = "bech32"
462511
version = "0.9.1"
@@ -1425,6 +1474,21 @@ dependencies = [
14251474
"ureq",
14261475
]
14271476

1477+
[[package]]
1478+
name = "esplora-client"
1479+
version = "0.12.3"
1480+
source = "registry+https://github.com/rust-lang/crates.io-index"
1481+
checksum = "f19e3ea99dbfbef0c1ec26d83e69de0c579f6aa6aaac4f44597805fcc27e97af"
1482+
dependencies = [
1483+
"bitcoin 0.32.8",
1484+
"hex-conservative 0.2.2",
1485+
"log",
1486+
"reqwest",
1487+
"serde",
1488+
"serde_json",
1489+
"tokio",
1490+
]
1491+
14281492
[[package]]
14291493
name = "extend"
14301494
version = "1.2.0"
@@ -1743,6 +1807,7 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
17431807
dependencies = [
17441808
"ahash",
17451809
"allocator-api2",
1810+
"serde",
17461811
]
17471812

17481813
[[package]]
@@ -2398,6 +2463,17 @@ dependencies = [
23982463
"serde",
23992464
]
24002465

2466+
[[package]]
2467+
name = "miniscript"
2468+
version = "12.3.5"
2469+
source = "registry+https://github.com/rust-lang/crates.io-index"
2470+
checksum = "487906208f38448e186e3deb02f2b8ef046a9078b0de00bdb28bf4fb9b76951c"
2471+
dependencies = [
2472+
"bech32 0.11.0",
2473+
"bitcoin 0.32.8",
2474+
"serde",
2475+
]
2476+
24012477
[[package]]
24022478
name = "miniz_oxide"
24032479
version = "0.8.9"
@@ -2753,10 +2829,13 @@ version = "0.2.0"
27532829
dependencies = [
27542830
"anyhow",
27552831
"async-trait",
2832+
"bdk_esplora",
2833+
"bdk_wallet",
27562834
"bitcoind-async-client",
27572835
"clap 4.5.46",
27582836
"config",
27592837
"dirs",
2838+
"esplora-client 0.12.3",
27602839
"http-body-util",
27612840
"hyper",
27622841
"hyper-util",

contrib/coverage.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ cargo llvm-cov clean --workspace
77
# exclude payjoin-ffi because bindings are tested in their native language and fuzz because these tests are not coverage worthy
88
cargo llvm-cov --no-report --workspace --all-features --exclude payjoin-ffi --exclude payjoin-fuzz
99
# Explicitly run payjoin-cli v1 e2e tests
10-
cargo llvm-cov --no-report --package payjoin-cli --no-default-features --features=v1,_manual-tls
10+
cargo llvm-cov --no-report --package payjoin-cli --no-default-features --features=v1,_manual-tls,bitcoind
11+
# Explicitly run payjoin-cli esplora tests
12+
cargo llvm-cov --no-report --package payjoin-cli --no-default-features --features=v1,v2,_manual-tls,esplora
1113
# generate report without tests
1214
cargo llvm-cov report --lcov --output-path lcov.info

payjoin-cli/Cargo.toml

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,20 @@ path = "src/main.rs"
1919

2020
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2121
[features]
22-
default = ["v2"]
22+
default = ["v2", "esplora"]
2323
native-certs = ["reqwest/rustls-tls-native-roots"]
2424
_manual-tls = ["reqwest/rustls-tls", "payjoin/_manual-tls", "tokio-rustls"]
2525
v1 = ["payjoin/v1", "hyper", "hyper-util", "http-body-util"]
2626
v2 = ["payjoin/v2", "payjoin/io"]
27+
esplora = ["bdk_wallet", "bdk_esplora", "esplora-client"]
28+
bitcoind = ["bitcoind-async-client"]
2729

2830
[dependencies]
2931
anyhow = "1.0.99"
3032
async-trait = "0.1.89"
31-
bitcoind-async-client = "0.10.2"
3233
clap = { version = "4.5.45", features = ["derive"] }
3334
config = "0.15.14"
3435
dirs = "6.0.0"
35-
http-body-util = { version = "0.1.3", optional = true }
36-
hyper = { version = "1.6.0", features = ["http1", "server"], optional = true }
37-
hyper-util = { version = "0.1.16", optional = true }
3836
payjoin = { version = "0.25.0", default-features = false }
3937
r2d2 = "0.8.10"
4038
r2d2_sqlite = "0.22.0"
@@ -46,13 +44,31 @@ rusqlite = { version = "0.29.0", features = ["bundled"] }
4644
serde = { version = "1.0.219", features = ["derive"] }
4745
serde_json = "1.0.149"
4846
tokio = { version = "1.47.1", features = ["full"] }
49-
tokio-rustls = { version = "0.26.2", features = [
50-
"ring",
51-
], default-features = false, optional = true }
5247
tracing = "0.1.41"
5348
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
5449
url = { version = "2.5.4", features = ["serde"] }
5550

51+
# Optional dependencies based on features
52+
bdk_esplora = { version = "0.22", default-features = false, features = [
53+
"async-https-rustls",
54+
"std",
55+
"tokio",
56+
], optional = true }
57+
bdk_wallet = { version = "3.0.0-rc.2", optional = true }
58+
bitcoind-async-client = { version = "0.10.2", optional = true }
59+
esplora-client = { version = "0.12", default-features = false, features = [
60+
"async-https-rustls",
61+
"tokio",
62+
], optional = true }
63+
64+
# TLS support
65+
http-body-util = { version = "0.1.3", optional = true }
66+
hyper = { version = "1.6.0", features = ["http1", "server"], optional = true }
67+
hyper-util = { version = "0.1.16", optional = true }
68+
tokio-rustls = { version = "0.26.2", features = [
69+
"ring",
70+
], default-features = false, optional = true }
71+
5672
[dev-dependencies]
5773
nix = { version = "0.30.1", features = ["aio", "process", "signal"] }
5874
payjoin-test-utils = { version = "0.0.1" }

payjoin-cli/contrib/lint.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
#!/usr/bin/env bash
22
set -e
33

4-
# Individual features with no defaults.
5-
features=("v1" "v2")
4+
# Protocol versions and wallet backends are orthogonal; every version must
5+
# build against every backend, so lint them as a matrix.
6+
versions=("v1" "v2")
7+
backends=("bitcoind" "esplora")
68

7-
for feature in "${features[@]}"; do
8-
# Don't duplicate --all-targets clippy. Clilppy end-user code, not tests.
9-
cargo clippy --no-default-features --features "$feature" -- -D warnings
9+
for version in "${versions[@]}"; do
10+
for backend in "${backends[@]}"; do
11+
# Don't duplicate --all-targets clippy. Clippy end-user code, not tests.
12+
cargo clippy --no-default-features --features "$version,$backend" -- -D warnings
13+
done
1014
done

payjoin-cli/contrib/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/usr/bin/env bash
22
set -e
33

4-
cargo test --locked --package payjoin-cli --verbose --all-features
4+
cargo test --locked --package payjoin-cli --verbose --no-default-features --features "v1,v2,_manual-tls,bitcoind,native-certs"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
##
2+
## Payjoin config.toml configuration file. Lines beginning with # are comments.
3+
##
4+
5+
# Common Settings
6+
# --------------
7+
8+
# The path to the database file
9+
db_path = "payjoin.sqlite"
10+
11+
# The maximum fee rate that the receiver is willing to pay (in sat/vB)
12+
max_fee_rate = 2.0
13+
14+
# Wallet Configuration
15+
# -------------------
16+
# Choose ONE backend: esplora or bitcoind
17+
#
18+
# Esplora backend (uses bdk_wallet with esplora API - no full node required):
19+
# [wallet.esplora]
20+
# descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/84'/1'/0'/0/*)"
21+
# # Optional: change descriptor (auto-derived if not specified)
22+
# # change_descriptor = "wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/84'/1'/0'/1/*)"
23+
# esplora_url = "https://blockstream.info/api"
24+
# network = "testnet"
25+
#
26+
# Bitcoind backend (uses Bitcoin Core RPC - requires full node):
27+
# [wallet.bitcoind]
28+
# rpchost = "http://localhost:18443/wallet/payjoin"
29+
# # Optional: cookie file path (if not using rpcuser/rpcpassword)
30+
# # cookie = "~/.bitcoin/testnet4/.cookie"
31+
# # Optional: RPC credentials (if not using cookie)
32+
# # rpcuser = "user"
33+
# # rpcpassword = "password"
34+
35+
# Version Configuration
36+
# -------------------
37+
# Uncomment ONE of the following version configurations depending on which version you want to use
38+
39+
# Version 1 Configuration
40+
# [v1]
41+
# port = 3000
42+
# pj_endpoint = "https://localhost:3000"
43+
44+
# Version 2 Configuration
45+
# [v2]
46+
# pj_directory = "https://payjo.in"
47+
# ohttp_relays = ["https://pj.benalleng.com", "https://pj.bobspacebkk.com", "https://ohttp.achow101.com"]

payjoin-cli/src/app/config.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const CONFIG_DIR: &str = "payjoin-cli";
1515

1616
type Builder = config::builder::ConfigBuilder<DefaultState>;
1717

18+
#[cfg(all(feature = "bitcoind", not(feature = "esplora")))]
1819
#[derive(Debug, Clone, Deserialize)]
1920
pub struct BitcoindConfig {
2021
pub rpchost: Url,
@@ -23,6 +24,15 @@ pub struct BitcoindConfig {
2324
pub rpcpassword: String,
2425
}
2526

27+
#[cfg(feature = "esplora")]
28+
#[derive(Debug, Clone, Deserialize)]
29+
pub struct BdkWalletConfig {
30+
pub descriptor: Option<String>,
31+
pub change_descriptor: Option<String>,
32+
pub esplora_url: Option<String>,
33+
pub network: Option<String>,
34+
}
35+
2636
#[cfg(feature = "v1")]
2737
#[derive(Debug, Clone, Deserialize)]
2838
pub struct V1Config {
@@ -55,7 +65,10 @@ pub enum VersionConfig {
5565
pub struct Config {
5666
pub db_path: PathBuf,
5767
pub max_fee_rate: Option<FeeRate>,
68+
#[cfg(all(feature = "bitcoind", not(feature = "esplora")))]
5869
pub bitcoind: BitcoindConfig,
70+
#[cfg(feature = "esplora")]
71+
pub wallet: Option<BdkWalletConfig>,
5972
#[serde(skip)]
6073
pub version: Option<VersionConfig>,
6174
#[cfg(feature = "_manual-tls")]
@@ -102,7 +115,7 @@ impl Config {
102115

103116
pub(crate) fn new(cli: &Cli) -> Result<Self, ConfigError> {
104117
let mut config = config::Config::builder();
105-
config = add_bitcoind_defaults(config, cli)?;
118+
config = add_wallet_defaults(config, cli)?;
106119
config = add_common_defaults(config, cli)?;
107120

108121
let version = Self::determine_version(cli)?;
@@ -143,7 +156,10 @@ impl Config {
143156
let mut config = Config {
144157
db_path: built_config.get("db_path")?,
145158
max_fee_rate: built_config.get("max_fee_rate").ok(),
159+
#[cfg(all(feature = "bitcoind", not(feature = "esplora")))]
146160
bitcoind: built_config.get("bitcoind")?,
161+
#[cfg(feature = "esplora")]
162+
wallet: built_config.get("wallet").ok(),
147163
version: None,
148164
#[cfg(feature = "_manual-tls")]
149165
root_certificate: built_config.get("root_certificate").ok(),
@@ -223,16 +239,35 @@ impl Config {
223239
}
224240
}
225241

226-
/// Set up default values and CLI overrides for Bitcoin RPC connection settings
227-
fn add_bitcoind_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
228-
// Set default values
242+
/// Set up default values and CLI overrides for wallet settings
243+
#[cfg(feature = "esplora")]
244+
fn add_wallet_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
245+
let config = config
246+
.set_default("wallet.descriptor", None::<String>)?
247+
.set_default("wallet.change_descriptor", None::<String>)?
248+
.set_default("wallet.esplora_url", None::<String>)?
249+
.set_default("wallet.network", None::<String>)?;
250+
251+
let descriptor = cli.descriptor.as_deref();
252+
let change_descriptor = cli.change_descriptor.as_deref();
253+
let esplora_url = cli.esplora_url.as_deref();
254+
let network = cli.network.as_deref();
255+
256+
config
257+
.set_override_option("wallet.descriptor", descriptor)?
258+
.set_override_option("wallet.change_descriptor", change_descriptor)?
259+
.set_override_option("wallet.esplora_url", esplora_url)?
260+
.set_override_option("wallet.network", network)
261+
}
262+
263+
#[cfg(all(feature = "bitcoind", not(feature = "esplora")))]
264+
fn add_wallet_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
229265
let config = config
230266
.set_default("bitcoind.rpchost", "http://localhost:18443")?
231267
.set_default("bitcoind.cookie", None::<String>)?
232268
.set_default("bitcoind.rpcuser", "bitcoin")?
233269
.set_default("bitcoind.rpcpassword", "")?;
234270

235-
// Override config values with command line arguments if applicable
236271
let rpchost = cli.rpchost.as_ref().map(|s| s.as_str());
237272
let cookie_file = cli.cookie_file.as_ref().map(|p| p.to_string_lossy().into_owned());
238273
let rpcuser = cli.rpcuser.as_deref();
@@ -245,6 +280,9 @@ fn add_bitcoind_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigEr
245280
.set_override_option("bitcoind.rpcpassword", rpcpassword)
246281
}
247282

283+
#[cfg(all(not(feature = "esplora"), not(feature = "bitcoind")))]
284+
fn add_wallet_defaults(config: Builder, _cli: &Cli) -> Result<Builder, ConfigError> { Ok(config) }
285+
248286
fn add_common_defaults(config: Builder, cli: &Cli) -> Result<Builder, ConfigError> {
249287
let db_path = cli.db_path.as_ref().map(|p| p.to_string_lossy().into_owned());
250288
config.set_default("db_path", db::DB_PATH)?.set_override_option("db_path", db_path)

0 commit comments

Comments
 (0)