]> Untitled Git - bdk-cli/commitdiff
feat: add payjoin send support
authorMehmet Efe Umit <mehmetefeumit@protonmail.com>
Wed, 19 Nov 2025 06:22:44 +0000 (22:22 -0800)
committerMehmet Efe Umit <mehmetefeumit@protonmail.com>
Thu, 18 Dec 2025 16:11:02 +0000 (08:11 -0800)
temp: move the send_payjoin out of the commit

Cargo.lock
Cargo.toml
src/commands.rs
src/error.rs
src/handlers.rs
src/main.rs
src/payjoin/mod.rs [new file with mode: 0644]
src/payjoin/ohttp.rs [new file with mode: 0644]

index 4ff8e29c74b6445c90ca0b1846205a8ba808a929..5e615eadf6ddc6c320f294a8d2a798bed1532adf 100644 (file)
@@ -17,6 +17,52 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
+[[package]]
+name = "aead"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
+dependencies = [
+ "cfg-if",
+ "cipher 0.3.0",
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f"
+dependencies = [
+ "aead 0.4.3",
+ "aes",
+ "cipher 0.3.0",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
 [[package]]
 name = "ahash"
 version = "0.8.12"
@@ -192,12 +238,15 @@ dependencies = [
  "dirs",
  "env_logger",
  "log",
+ "payjoin",
+ "reqwest",
  "serde_json",
  "shlex",
- "thiserror",
+ "thiserror 2.0.12",
  "tokio",
  "tracing",
  "tracing-subscriber",
+ "url",
 ]
 
 [[package]]
@@ -278,7 +327,7 @@ dependencies = [
  "ciborium",
  "redb",
  "tempfile",
- "thiserror",
+ "thiserror 2.0.12",
 ]
 
 [[package]]
@@ -291,7 +340,7 @@ dependencies = [
  "bip39",
  "bitcoin",
  "miniscript",
- "rand_core",
+ "rand_core 0.6.4",
  "serde",
  "serde_json",
 ]
@@ -302,6 +351,15 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
 
+[[package]]
+name = "bhttp"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16fc24bc615b9fd63148f59b218ea58a444b55762f8845da910e23aca686398b"
+dependencies = [
+ "thiserror 1.0.69",
+]
+
 [[package]]
 name = "bindgen"
 version = "0.69.5"
@@ -319,7 +377,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "regex",
- "rustc-hash",
+ "rustc-hash 1.1.0",
  "shlex",
  "syn",
  "which",
@@ -346,7 +404,7 @@ dependencies = [
  "bitcoin",
  "bitcoin_hashes 0.15.0",
  "chacha20-poly1305",
- "rand",
+ "rand 0.8.5",
  "tokio",
 ]
 
@@ -389,6 +447,25 @@ dependencies = [
  "bitcoin",
 ]
 
+[[package]]
+name = "bitcoin-hpke"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d37a54c486727c1d1ae9cc28dcf78b6e6ba20dcb88e8c892f1437d9ce215dc8c"
+dependencies = [
+ "aead 0.5.2",
+ "chacha20poly1305 0.10.1",
+ "digest 0.10.7",
+ "generic-array",
+ "hkdf 0.12.4",
+ "hmac 0.12.1",
+ "rand_core 0.6.4",
+ "secp256k1",
+ "sha2 0.10.9",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "bitcoin-internals"
 version = "0.2.0"
@@ -425,6 +502,29 @@ dependencies = [
  "bitcoin-internals 0.4.0",
 ]
 
+[[package]]
+name = "bitcoin-ohttp"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87a803a4b54e44635206b53329c78c0029d0c70926288ac2f07f4bb1267546cb"
+dependencies = [
+ "aead 0.4.3",
+ "aes-gcm",
+ "bitcoin-hpke",
+ "byteorder",
+ "chacha20poly1305 0.8.0",
+ "hex",
+ "hkdf 0.11.0",
+ "lazy_static",
+ "log",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "sha2 0.9.9",
+ "thiserror 1.0.69",
+ "toml",
+]
+
 [[package]]
 name = "bitcoin-units"
 version = "0.1.2"
@@ -466,6 +566,16 @@ dependencies = [
  "hex-conservative 0.3.0",
 ]
 
+[[package]]
+name = "bitcoin_uri"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e0a228e083d1702f83389b0ac71eb70078dc8d7fcbb6cde864d1cbca145f5cc"
+dependencies = [
+ "bitcoin",
+ "percent-encoding-rfc3986",
+]
+
 [[package]]
 name = "bitcoincore-rpc"
 version = "0.19.0"
@@ -496,6 +606,24 @@ version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.19.0"
@@ -540,12 +668,67 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
 
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412"
+dependencies = [
+ "cfg-if",
+ "cipher 0.3.0",
+ "cpufeatures 0.1.5",
+ "zeroize",
+]
+
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher 0.4.4",
+ "cpufeatures 0.2.17",
+]
+
 [[package]]
 name = "chacha20-poly1305"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4b4b0fc281743d80256607bd65e8beedc42cb0787ea119c85b81b4c0eab85e5f"
 
+[[package]]
+name = "chacha20poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5"
+dependencies = [
+ "aead 0.4.3",
+ "chacha20 0.7.1",
+ "cipher 0.3.0",
+ "poly1305 0.7.2",
+ "zeroize",
+]
+
+[[package]]
+name = "chacha20poly1305"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
+dependencies = [
+ "aead 0.5.2",
+ "chacha20 0.9.1",
+ "cipher 0.4.4",
+ "poly1305 0.8.0",
+ "zeroize",
+]
+
 [[package]]
 name = "ciborium"
 version = "0.2.2"
@@ -573,6 +756,26 @@ dependencies = [
  "half",
 ]
 
+[[package]]
+name = "cipher"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+ "zeroize",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.8.1"
@@ -678,12 +881,51 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
+[[package]]
+name = "cpufeatures"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "crunchy"
 version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
 
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
 [[package]]
 name = "csv"
 version = "1.3.1"
@@ -705,6 +947,35 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ctr"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481"
+dependencies = [
+ "cipher 0.3.0",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "dirs"
 version = "6.0.0"
@@ -762,7 +1033,7 @@ dependencies = [
  "rustls 0.23.31",
  "serde",
  "serde_json",
- "webpki-roots",
+ "webpki-roots 0.25.4",
  "winapi",
 ]
 
@@ -957,6 +1228,16 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.16"
@@ -964,8 +1245,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "wasi 0.11.1+wasi-snapshot-preview1",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -975,9 +1258,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi",
  "wasi 0.14.2+wasi-0.2.4",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ghash"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99"
+dependencies = [
+ "opaque-debug",
+ "polyval",
 ]
 
 [[package]]
@@ -1027,6 +1322,12 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
 [[package]]
 name = "hex-conservative"
 version = "0.1.2"
@@ -1057,6 +1358,44 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
 
+[[package]]
+name = "hkdf"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
+dependencies = [
+ "digest 0.9.0",
+ "hmac 0.11.0",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac 0.12.1",
+]
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
 [[package]]
 name = "home"
 version = "0.5.11"
@@ -1125,6 +1464,23 @@ dependencies = [
  "want",
 ]
 
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls 0.23.31",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots 1.0.4",
+]
+
 [[package]]
 name = "hyper-tls"
 version = "0.6.0"
@@ -1272,6 +1628,15 @@ dependencies = [
  "icu_properties",
 ]
 
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "io-uring"
 version = "0.7.9"
@@ -1459,6 +1824,12 @@ version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
 
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
 [[package]]
 name = "memchr"
 version = "2.7.5"
@@ -1503,7 +1874,7 @@ dependencies = [
  "rustls-webpki 0.101.7",
  "serde",
  "serde_json",
- "webpki-roots",
+ "webpki-roots 0.25.4",
 ]
 
 [[package]]
@@ -1574,6 +1945,12 @@ version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
 
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
 [[package]]
 name = "openssl"
 version = "0.10.73"
@@ -1647,12 +2024,37 @@ dependencies = [
  "windows-targets 0.52.6",
 ]
 
+[[package]]
+name = "payjoin"
+version = "1.0.0-rc.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e20b76ae28f1420a918e8051681fc9669ed7273e542e515baa329be78c3255a"
+dependencies = [
+ "bhttp",
+ "bitcoin",
+ "bitcoin-hpke",
+ "bitcoin-ohttp",
+ "bitcoin_uri",
+ "http",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tracing",
+ "url",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "percent-encoding-rfc3986"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3637c05577168127568a64e9dc5a6887da720efef07b3d9472d45f63ab191166"
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.16"
@@ -1671,6 +2073,40 @@ version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
 
+[[package]]
+name = "poly1305"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede"
+dependencies = [
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash 0.4.0",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash 0.5.1",
+]
+
+[[package]]
+name = "polyval"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash 0.4.0",
+]
+
 [[package]]
 name = "portable-atomic"
 version = "1.11.1"
@@ -1723,6 +2159,61 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.1",
+ "rustls 0.23.31",
+ "socket2",
+ "thiserror 2.0.12",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.3",
+ "lru-slab",
+ "rand 0.9.2",
+ "ring",
+ "rustc-hash 2.1.1",
+ "rustls 0.23.31",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.12",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.40"
@@ -1745,8 +2236,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -1756,7 +2257,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
 ]
 
 [[package]]
@@ -1768,6 +2279,15 @@ dependencies = [
  "getrandom 0.2.16",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
 [[package]]
 name = "redb"
 version = "2.6.0"
@@ -1794,7 +2314,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
 dependencies = [
  "getrandom 0.2.16",
  "libredox",
- "thiserror",
+ "thiserror 2.0.12",
 ]
 
 [[package]]
@@ -1828,9 +2348,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
 name = "reqwest"
-version = "0.12.22"
+version = "0.12.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
+checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
 dependencies = [
  "base64 0.22.1",
  "bytes",
@@ -1839,6 +2359,7 @@ dependencies = [
  "http-body",
  "http-body-util",
  "hyper",
+ "hyper-rustls",
  "hyper-tls",
  "hyper-util",
  "js-sys",
@@ -1846,6 +2367,8 @@ dependencies = [
  "native-tls",
  "percent-encoding",
  "pin-project-lite",
+ "quinn",
+ "rustls 0.23.31",
  "rustls-pki-types",
  "serde",
  "serde_json",
@@ -1853,6 +2376,7 @@ dependencies = [
  "sync_wrapper",
  "tokio",
  "tokio-native-tls",
+ "tokio-rustls",
  "tower",
  "tower-http",
  "tower-service",
@@ -1860,6 +2384,7 @@ dependencies = [
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
+ "webpki-roots 1.0.4",
 ]
 
 [[package]]
@@ -1902,6 +2427,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
 [[package]]
 name = "rustix"
 version = "0.38.44"
@@ -1949,6 +2480,7 @@ dependencies = [
  "aws-lc-rs",
  "log",
  "once_cell",
+ "ring",
  "rustls-pki-types",
  "rustls-webpki 0.103.4",
  "subtle",
@@ -1961,6 +2493,7 @@ version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
 dependencies = [
+ "web-time",
  "zeroize",
 ]
 
@@ -2030,7 +2563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
 dependencies = [
  "bitcoin_hashes 0.14.0",
- "rand",
+ "rand 0.8.5",
  "secp256k1-sys",
  "serde",
 ]
@@ -2111,6 +2644,30 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
 [[package]]
 name = "sharded-slab"
 version = "0.1.7"
@@ -2228,13 +2785,33 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
 [[package]]
 name = "thiserror"
 version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
 ]
 
 [[package]]
@@ -2323,6 +2900,25 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls 0.23.31",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "tower"
 version = "0.5.2"
@@ -2431,6 +3027,12 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.18"
@@ -2452,6 +3054,26 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
 
+[[package]]
+name = "universal-hash"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
 [[package]]
 name = "untrusted"
 version = "0.9.0"
@@ -2467,6 +3089,7 @@ dependencies = [
  "form_urlencoded",
  "idna",
  "percent-encoding",
+ "serde",
 ]
 
 [[package]]
@@ -2604,12 +3227,31 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "webpki-roots"
 version = "0.25.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
 
+[[package]]
+name = "webpki-roots"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
+dependencies = [
+ "rustls-pki-types",
+]
+
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -2900,6 +3542,20 @@ name = "zeroize"
 version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
 
 [[package]]
 name = "zerotrie"
index d5767f31e754de3650df009264315c31c423accc..366ee5bf8430233491c3f2ca5e257ba2fdac2c50 100644 (file)
@@ -21,6 +21,8 @@ serde_json = "1.0"
 thiserror = "2.0.11"
 tokio = { version = "1", features = ["full"] }
 cli-table = "0.5.0"
+tracing = "0.1.41"
+tracing-subscriber = "0.3.20"
 
 # Optional dependencies
 bdk_bitcoind_rpc = { version = "0.21.0", features = ["std"], optional = true }
@@ -29,8 +31,9 @@ bdk_esplora = { version = "0.22.1", features = ["async-https", "tokio"], optiona
 bdk_kyoto = { version = "0.15.1", optional = true }
 bdk_redb = { version = "0.1.0", optional = true }
 shlex = {  version = "1.3.0", optional = true }
-tracing = "0.1.41"
-tracing-subscriber = "0.3.20"
+payjoin = { version = "1.0.0-rc.1", features = ["v1", "v2", "io", "_test-utils"], optional = true}
+reqwest = { version = "0.12.23", default-features = false, optional = true }
+url = { version = "2.5.4", optional = true }
 
 [features]
 default = ["repl", "sqlite"]
@@ -43,10 +46,13 @@ sqlite = ["bdk_wallet/rusqlite"]
 redb = ["bdk_redb"]
 
 # Available blockchain client options
-cbf = ["bdk_kyoto"]
-electrum = ["bdk_electrum"]
-esplora = ["bdk_esplora"]
-rpc = ["bdk_bitcoind_rpc"]
+cbf = ["bdk_kyoto", "_payjoin-dependencies"]
+electrum = ["bdk_electrum", "_payjoin-dependencies"]
+esplora = ["bdk_esplora", "_payjoin-dependencies"]
+rpc = ["bdk_bitcoind_rpc", "_payjoin-dependencies"] 
+
+# Internal features
+_payjoin-dependencies = ["payjoin", "reqwest", "url"]
 
 # Use this to consensus verify transactions at sync time
 verify = []
index d3f2d98313a7ffd62f7396528024339008382a0f..e730c0f9532adebcd8e3b29a68c173097e2f26ab 100644 (file)
@@ -430,6 +430,24 @@ pub enum OnlineWalletSubCommand {
         )]
         tx: Option<String>,
     },
+    /// Sends an original PSBT to a BIP 21 URI and broadcasts the returned Payjoin PSBT.
+    SendPayjoin {
+        /// BIP 21 URI for the Payjoin.
+        #[arg(env = "PAYJOIN_URI", long = "uri", required = true)]
+        uri: String,
+        /// URL of the Payjoin OHTTP relay. Can be repeated multiple times to attempt the
+        /// operation with multiple relays for redundancy.
+        #[arg(env = "PAYJOIN_OHTTP_RELAY", long = "ohttp_relay", required = true)]
+        ohttp_relay: Vec<String>,
+        /// Fee rate to use in sat/vbyte.
+        #[arg(
+            env = "PAYJOIN_SENDER_FEE_RATE",
+            short = 'f',
+            long = "fee_rate",
+            required = true
+        )]
+        fee_rate: u64,
+    },
 }
 
 /// Subcommands for Key operations.
index 1b8b5b456a9f6d1525a0b95c3b26a65ea1146653..064a928d2ad82fa0a97f48538abc9ad4ca89dbcf 100644 (file)
@@ -103,6 +103,15 @@ pub enum BDKCliError {
     #[cfg(feature = "cbf")]
     #[error("BDK-Kyoto update error: {0}")]
     KyotoUpdateError(#[from] bdk_kyoto::UpdateError),
+
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "rpc",
+        feature = "cbf",
+    ))]
+    #[error("Reqwest error: {0}")]
+    ReqwestError(#[from] reqwest::Error),
 }
 
 impl From<ExtractTxError> for BDKCliError {
index b863e25b7c0747dd3dae7a509488f6087522ec9b..d9b214ccc8abd28d96f921b05fdd6ca8c34642fa 100644 (file)
@@ -58,7 +58,14 @@ use std::convert::TryFrom;
 #[cfg(any(feature = "repl", feature = "electrum", feature = "esplora"))]
 use std::io::Write;
 use std::str::FromStr;
-#[cfg(any(feature = "redb", feature = "compiler"))]
+#[cfg(any(
+    feature = "redb",
+    feature = "compiler",
+    feature = "electrum",
+    feature = "esplora",
+    feature = "cbf",
+    feature = "rpc"
+))]
 use std::sync::Arc;
 #[cfg(any(
     feature = "electrum",
@@ -68,7 +75,9 @@ use std::sync::Arc;
 ))]
 use {
     crate::commands::OnlineWalletSubCommand::*,
+    crate::payjoin::{PayjoinManager, ohttp::RelayManager},
     bdk_wallet::bitcoin::{Transaction, consensus::Decodable, hex::FromHex},
+    std::sync::Mutex,
 };
 #[cfg(feature = "esplora")]
 use {crate::utils::BlockchainClient::Esplora, bdk_esplora::EsploraAsyncExt};
@@ -706,6 +715,17 @@ pub(crate) async fn handle_online_wallet_subcommand(
             let txid = broadcast_transaction(client, tx).await?;
             Ok(serde_json::to_string_pretty(&json!({ "txid": txid }))?)
         }
+        SendPayjoin {
+            uri,
+            ohttp_relay,
+            fee_rate,
+        } => {
+            let relay_manager = Arc::new(Mutex::new(RelayManager::new()));
+            let mut payjoin_manager = PayjoinManager::new(wallet, relay_manager);
+            return payjoin_manager
+                .send_payjoin(uri, fee_rate, ohttp_relay, client)
+                .await;
+        }
     }
 }
 
index c69aecc7f27465640873bb7355fde1fabe97f164..81190bfe01307b5d231fd30a5d561f00451f41b4 100644 (file)
 mod commands;
 mod error;
 mod handlers;
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "cbf",
+    feature = "rpc"
+))]
+mod payjoin;
 #[cfg(any(feature = "sqlite", feature = "redb"))]
 mod persister;
 mod utils;
diff --git a/src/payjoin/mod.rs b/src/payjoin/mod.rs
new file mode 100644 (file)
index 0000000..2f36224
--- /dev/null
@@ -0,0 +1,274 @@
+use crate::error::BDKCliError as Error;
+use crate::handlers::{broadcast_transaction, sync_wallet};
+use crate::utils::BlockchainClient;
+use bdk_wallet::{
+    SignOptions, Wallet,
+    bitcoin::{FeeRate, Psbt, Txid, consensus::encode::serialize_hex},
+};
+use payjoin::bitcoin::TxIn;
+use payjoin::persist::{OptionalTransitionOutcome, SessionPersister};
+use payjoin::receive::InputPair;
+use payjoin::receive::v2::{
+    HasReplyableError, Initialized, MaybeInputsOwned, MaybeInputsSeen, Monitor, OutputsUnknown,
+    PayjoinProposal, ProvisionalProposal, ReceiveSession, Receiver,
+    SessionEvent as ReceiverSessionEvent, UncheckedOriginalPayload, WantsFeeRange, WantsInputs,
+    WantsOutputs,
+};
+use payjoin::send::v2::{
+    PollingForProposal, SendSession, Sender, SessionEvent as SenderSessionEvent,
+    SessionOutcome as SenderSessionOutcome, WithReplyKey,
+};
+use payjoin::{ImplementationError, UriExt};
+use serde_json::{json, to_string_pretty};
+use std::sync::{Arc, Mutex};
+
+use crate::payjoin::ohttp::{RelayManager, fetch_ohttp_keys};
+
+pub mod ohttp;
+
+/// Implements all of the functions required to go through the Payjoin receive and send processes.
+///
+/// TODO: At the time of writing, this struct is written to make a Persister implementation easier
+/// but the persister is not implemented yet! For instance [`PayjoinManager::proceed_sender_session`] and
+/// [`PayjoinManager::proceed_receiver_session`] are designed such that the manager can enable
+/// resuming ongoing payjoins are well. So... this is a TODO for implementing persister.
+pub(crate) struct PayjoinManager<'a> {
+    wallet: &'a mut Wallet,
+    relay_manager: Arc<Mutex<RelayManager>>,
+}
+
+impl<'a> PayjoinManager<'a> {
+    pub fn new(wallet: &'a mut Wallet, relay_manager: Arc<Mutex<RelayManager>>) -> Self {
+        Self {
+            wallet,
+            relay_manager,
+        }
+    }
+
+
+    pub async fn send_payjoin(
+        &mut self,
+        uri: String,
+        fee_rate: u64,
+        ohttp_relays: Vec<String>,
+        blockchain_client: BlockchainClient,
+    ) -> Result<String, Error> {
+        let uri = payjoin::Uri::try_from(uri)
+            .map_err(|e| Error::Generic(format!("Failed parsing to Payjoin URI: {}", e)))?;
+        let uri = uri.require_network(self.wallet.network()).map_err(|e| {
+            Error::Generic(format!("Failed setting the right network for the URI: {e}"))
+        })?;
+        let uri = uri
+            .check_pj_supported()
+            .map_err(|e| Error::Generic(format!("URI does not support Payjoin: {}", e)))?;
+
+        let sats = uri
+            .amount
+            .ok_or_else(|| Error::Generic("Amount is not specified in the URI.".to_string()))?;
+
+        let fee_rate = FeeRate::from_sat_per_vb(fee_rate).expect("Provided fee rate is not valid.");
+
+        // Build and sign the original PSBT which pays to the receiver.
+        let mut original_psbt = {
+            let mut tx_builder = self.wallet.build_tx();
+            tx_builder
+                .add_recipient(uri.address.script_pubkey(), sats)
+                .fee_rate(fee_rate);
+
+            tx_builder.finish().map_err(|e| {
+                Error::Generic(format!(
+                    "Error occurred when building original Payjoin transaction: {e}"
+                ))
+            })?
+        };
+        if !self
+            .wallet
+            .sign(&mut original_psbt, SignOptions::default())?
+        {
+            return Err(Error::Generic(
+                "Failed to sign and finalize the original PSBT.".to_string(),
+            ));
+        }
+
+        let txid = match uri.extras.pj_param() {
+            payjoin::PjParam::V1(_) => {
+                let (req, ctx) = payjoin::send::v1::SenderBuilder::new(original_psbt.clone(), uri)
+                    .build_recommended(fee_rate)
+                    .map_err(|e| {
+                        Error::Generic(format!("Failed to build a Payjoin v1 sender: {e}"))
+                    })?
+                    .create_v1_post_request();
+
+                let response = self
+                    .send_payjoin_post_request(req)
+                    .await
+                    .map_err(|e| Error::Generic(format!("Failed to send request: {e}")))?;
+
+                let psbt = ctx
+                    .process_response(&response.bytes().await?)
+                    .map_err(|e| Error::Generic(format!("Failed to send a Payjoin v1: {e}")))?;
+
+                self.process_payjoin_proposal(psbt, blockchain_client)
+                    .await?
+            }
+            payjoin::PjParam::V2(_) => {
+                let ohttp_relays: Vec<url::Url> = ohttp_relays
+                    .into_iter()
+                    .map(|s| url::Url::parse(&s))
+                    .collect::<Result<_, _>>()
+                    .map_err(|e| {
+                        Error::Generic(format!("Failed to parse one or more OHTTP URLs: {e}"))
+                    })?;
+
+                if ohttp_relays.is_empty() {
+                    return Err(Error::Generic(
+                        "At least one valid OHTTP relay must be provided.".into(),
+                    ));
+                }
+
+                // TODO: Implement proper persister.
+                let persister =
+                    payjoin::persist::NoopSessionPersister::<SenderSessionEvent>::default();
+
+                let sender = payjoin::send::v2::SenderBuilder::new(original_psbt.clone(), uri)
+                    .build_recommended(fee_rate)
+                    .map_err(|e| {
+                        Error::Generic(format!("Failed to build a Payjoin v2 sender: {e}"))
+                    })?
+                    .save(&persister)
+                    .map_err(|e| {
+                        Error::Generic(format!(
+                            "Failed to save the Payjoin v2 sender in the persister: {e}"
+                        ))
+                    })?;
+
+                let selected_relay =
+                    fetch_ohttp_keys(ohttp_relays, &sender.endpoint(), self.relay_manager.clone())
+                        .await?
+                        .relay_url;
+
+                self.proceed_sender_session(
+                    SendSession::WithReplyKey(sender),
+                    &persister,
+                    selected_relay.to_string(),
+                    blockchain_client,
+                )
+                .await?
+            }
+            _ => {
+                unimplemented!("Payjoin version not recognized.");
+            }
+        };
+
+        Ok(to_string_pretty(&json!({ "txid": txid }))?)
+    }
+    async fn proceed_sender_session(
+        &self,
+        session: SendSession,
+        persister: &impl SessionPersister<SessionEvent = SenderSessionEvent>,
+        relay: impl payjoin::IntoUrl,
+        blockchain_client: BlockchainClient,
+    ) -> Result<Txid, Error> {
+        match session {
+            SendSession::WithReplyKey(context) => {
+                self.post_original_proposal(context, relay, persister, blockchain_client)
+                    .await
+            }
+            SendSession::PollingForProposal(context) => {
+                self.get_proposed_payjoin_proposal(context, relay, persister, blockchain_client)
+                    .await
+            }
+            SendSession::Closed(SenderSessionOutcome::Success(psbt)) => {
+                self.process_payjoin_proposal(psbt, blockchain_client).await
+            }
+            _ => Err(Error::Generic("Unexpected SendSession state!".to_string())),
+        }
+    }
+
+    async fn post_original_proposal(
+        &self,
+        sender: Sender<WithReplyKey>,
+        relay: impl payjoin::IntoUrl,
+        persister: &impl SessionPersister<SessionEvent = SenderSessionEvent>,
+        blockchain_client: BlockchainClient,
+    ) -> Result<Txid, Error> {
+        let (req, ctx) = sender.create_v2_post_request(relay.as_str()).map_err(|e| {
+            Error::Generic(format!(
+                "Failed to create a post request for a Payjoin send: {e}"
+            ))
+        })?;
+        let response = self.send_payjoin_post_request(req).await?;
+        let sender = sender
+            .process_response(&response.bytes().await?, ctx)
+            .save(persister)
+        .map_err(|e| {
+                Error::Generic(format!("Failed to persist the Payjoin send after successfully sending original proposal: {e}"))
+            })?;
+        self.get_proposed_payjoin_proposal(sender, relay, persister, blockchain_client)
+            .await
+    }
+
+    async fn get_proposed_payjoin_proposal(
+        &self,
+        sender: Sender<PollingForProposal>,
+        relay: impl payjoin::IntoUrl,
+        persister: &impl SessionPersister<SessionEvent = SenderSessionEvent>,
+        blockchain_client: BlockchainClient,
+    ) -> Result<Txid, Error> {
+        let mut sender = sender.clone();
+        loop {
+            let (req, ctx) = sender.create_poll_request(relay.as_str()).map_err(|e| {
+                Error::Generic(format!(
+                    "Failed to create a poll request during a Payjoin send: {e}"
+                ))
+            })?;
+            let response = self.send_payjoin_post_request(req).await?;
+            let processed_response = sender
+                .process_response(&response.bytes().await?, ctx)
+                .save(persister);
+            match processed_response {
+                Ok(OptionalTransitionOutcome::Progress(psbt)) => {
+                    println!("Proposal received. Processing...");
+                    return self.process_payjoin_proposal(psbt, blockchain_client).await;
+                }
+                Ok(OptionalTransitionOutcome::Stasis(current_state)) => {
+                    println!("No response yet. Continuing polling...");
+                    sender = current_state;
+                    continue;
+                }
+                Err(e) => {
+                    break Err(Error::Generic(format!(
+                        "Error occurred when polling for Payjoin v2 proposal: {e}"
+                    )));
+                }
+            }
+        }
+    }
+
+    async fn process_payjoin_proposal(
+        &self,
+        mut psbt: Psbt,
+        blockchain_client: BlockchainClient,
+    ) -> Result<Txid, Error> {
+        if !self.wallet.sign(&mut psbt, SignOptions::default())? {
+            return Err(Error::Generic(
+                "Failed to sign and finalize the Payjoin proposal PSBT.".to_string(),
+            ));
+        }
+
+        broadcast_transaction(blockchain_client, psbt.extract_tx_fee_rate_limit()?).await
+    }
+
+    async fn send_payjoin_post_request(
+        &self,
+        req: payjoin::Request,
+    ) -> reqwest::Result<reqwest::Response> {
+        let client = reqwest::Client::new();
+        client
+            .post(req.url)
+            .header("Content-Type", req.content_type)
+            .body(req.body)
+            .send()
+            .await
+    }
+}
diff --git a/src/payjoin/ohttp.rs b/src/payjoin/ohttp.rs
new file mode 100644 (file)
index 0000000..1cc0935
--- /dev/null
@@ -0,0 +1,110 @@
+use crate::error::BDKCliError as Error;
+use std::sync::{Arc, Mutex};
+
+#[derive(Debug, Clone)]
+pub(crate) struct RelayManager {
+    selected_relay: Option<url::Url>,
+    failed_relays: Vec<url::Url>,
+}
+
+impl RelayManager {
+    pub fn new() -> Self {
+        RelayManager {
+            selected_relay: None,
+            failed_relays: Vec::new(),
+        }
+    }
+
+    pub fn set_selected_relay(&mut self, relay: url::Url) {
+        self.selected_relay = Some(relay);
+    }
+
+    pub fn get_selected_relay(&self) -> Option<url::Url> {
+        self.selected_relay.clone()
+    }
+
+    pub fn add_failed_relay(&mut self, relay: url::Url) {
+        self.failed_relays.push(relay);
+    }
+
+    pub fn get_failed_relays(&self) -> Vec<url::Url> {
+        self.failed_relays.clone()
+    }
+}
+
+pub(crate) struct ValidatedOhttpKeys {
+    pub(crate) ohttp_keys: payjoin::OhttpKeys,
+    pub(crate) relay_url: url::Url,
+}
+
+pub(crate) async fn fetch_ohttp_keys(
+    relays: Vec<url::Url>,
+    payjoin_directory: impl payjoin::IntoUrl,
+    relay_manager: Arc<Mutex<RelayManager>>,
+) -> Result<ValidatedOhttpKeys, Error> {
+    use payjoin::bitcoin::secp256k1::rand::prelude::SliceRandom;
+
+    loop {
+        let failed_relays = relay_manager
+            .lock()
+            .expect("Lock should not be poisoned")
+            .get_failed_relays();
+
+        let remaining_relays: Vec<_> = relays
+            .iter()
+            .filter(|r| !failed_relays.contains(r))
+            .cloned()
+            .collect();
+
+        if remaining_relays.is_empty() {
+            return Err(Error::Generic(
+                "No valid OHTTP relays available".to_string(),
+            ));
+        }
+
+        let selected_relay =
+            match remaining_relays.choose(&mut payjoin::bitcoin::key::rand::thread_rng()) {
+                Some(relay) => relay.clone(),
+                None => {
+                    return Err(Error::Generic(
+                        "Failed to select from remaining relays".to_string(),
+                    ));
+                }
+            };
+
+        relay_manager
+            .lock()
+            .expect("Lock should not be poisoned")
+            .set_selected_relay(selected_relay.clone());
+
+        let ohttp_keys =
+            payjoin::io::fetch_ohttp_keys(selected_relay.as_str(), payjoin_directory.as_str())
+                .await;
+
+        match ohttp_keys {
+            Ok(keys) => {
+                return Ok(ValidatedOhttpKeys {
+                    ohttp_keys: keys,
+                    relay_url: selected_relay,
+                });
+            }
+            Err(payjoin::io::Error::UnexpectedStatusCode(e)) => {
+                return Err(Error::Generic(format!(
+                    "Unexpected error occurred when fetching OHTTP keys: {}",
+                    e
+                )));
+            }
+            Err(e) => {
+                tracing::debug!(
+                    "Failed to connect to OHTTP relay: {}, {}",
+                    selected_relay,
+                    e
+                );
+                relay_manager
+                    .lock()
+                    .expect("Lock should not be poisoned")
+                    .add_failed_relay(selected_relay);
+            }
+        }
+    }
+}