From 98cba409b772306f499c0aa7284301097ecb4f66 Mon Sep 17 00:00:00 2001 From: DanGould Date: Wed, 16 Oct 2024 12:26:05 -0400 Subject: [PATCH] Upgrade to unreleased payjoin-0.21 Still rely on git head rather than 0.21 release. --- Cargo.lock | 684 +++++++++++++++++------------- Cargo.toml | 7 +- README.md | 2 +- src/bitcoin.rs | 70 +++ src/error.rs | 25 +- src/lib.rs | 2 +- src/ohttp.rs | 17 + src/receive/v1.rs | 179 ++++---- src/receive/v2.rs | 360 ++++++++-------- src/request.rs | 20 +- src/send/mod.rs | 34 ++ src/send/v1.rs | 86 ++-- src/send/v2.rs | 61 ++- src/uri.rs | 4 +- tests/bdk_integration_test.rs | 370 ++++++++-------- tests/bitcoin_core_integration.rs | 153 ++++--- 16 files changed, 1173 insertions(+), 901 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3226e48..00bef28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", @@ -69,22 +69,8 @@ dependencies = [ "aead 0.4.3", "aes 0.7.5", "cipher 0.3.0", - "ctr 0.7.0", - "ghash 0.4.4", - "subtle", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead 0.5.2", - "aes 0.8.3", - "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.0", + "ctr", + "ghash", "subtle", ] @@ -249,6 +235,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "basic-toml" version = "0.1.4" @@ -268,6 +260,7 @@ dependencies = [ "bdk-macros", "bip39", "bitcoin 0.30.1", + "core-rpc", "electrum-client", "esplora-client", "getrandom", @@ -378,6 +371,25 @@ dependencies = [ "serde", ] +[[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", + "secp256k1 0.29.0", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "bitcoin-internals" version = "0.1.0" @@ -399,6 +411,29 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" +[[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", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "toml", +] + [[package]] name = "bitcoin-private" version = "0.1.0" @@ -449,7 +484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" dependencies = [ "bitcoincore-rpc-json", - "jsonrpc", + "jsonrpc 0.18.0", "log", "serde", "serde_json", @@ -466,6 +501,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bitcoind" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee5cf6a9903ff9cc808494c1232b0e9f6eef6600913d0d69fe1cb5c428f25b9" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.0", + "bitcoincore-rpc", + "flate2", + "log", + "minreq", + "tar", + "tempfile", + "which", + "zip", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -524,6 +577,27 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "camino" version = "1.1.6" @@ -562,6 +636,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -702,6 +777,12 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.4" @@ -718,6 +799,32 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-rpc" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d77079e1b71c2778d6e1daf191adadcd4ff5ec3ccad8298a79061d865b235b" +dependencies = [ + "bitcoin-private", + "core-rpc-json", + "jsonrpc 0.13.0", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "core-rpc-json" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581898ed9a83f31c64731b1d8ca2dfffcfec14edf1635afacd5234cddbde3a41" +dependencies = [ + "bitcoin 0.30.1", + "bitcoin-private", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.1.5" @@ -767,7 +874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "typenum", ] @@ -790,28 +897,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - [[package]] name = "darling" version = "0.13.4" @@ -882,6 +967,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "electrum-client" version = "0.18.0" @@ -902,10 +993,14 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.1" +name = "errno" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] [[package]] name = "esplora-client" @@ -920,6 +1015,24 @@ dependencies = [ "ureq", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.0.27" @@ -1087,17 +1200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" dependencies = [ "opaque-debug", - "polyval 0.5.3", -] - -[[package]] -name = "ghash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" -dependencies = [ - "opaque-debug", - "polyval 0.6.1", + "polyval", ] [[package]] @@ -1123,31 +1226,6 @@ dependencies = [ "scroll", ] -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "heck" version = "0.4.1" @@ -1226,35 +1304,12 @@ dependencies = [ ] [[package]] -name = "hpke" -version = "0.10.0" +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf39e5461bfdc6ad0fbc97067519fcaf96a7a2e67f24cc0eb8a1e7c0c45af792" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "aead 0.5.2", - "aes-gcm 0.10.3", - "byteorder", - "chacha20poly1305 0.10.1", - "digest 0.10.7", - "generic-array", - "hkdf 0.12.4", - "hmac 0.12.1", - "rand_core 0.6.4", - "sha2 0.10.8", - "subtle", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", + "windows-sys 0.52.0", ] [[package]] @@ -1268,17 +1323,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1286,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -1297,8 +1341,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1314,30 +1358,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.7", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.4.1" @@ -1347,8 +1367,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1358,22 +1378,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.30", - "log", - "rustls 0.21.7", - "rustls-native-certs 0.6.3", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.26.0" @@ -1381,15 +1385,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.4.1", + "http", + "hyper", "hyper-util", "log", "rustls 0.22.4", - "rustls-native-certs 0.7.1", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", "tower-service", "webpki-roots 0.26.3", ] @@ -1401,7 +1405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a343d17fe7885302ed7252767dc7bb83609a874b6ff581142241ec4b73957ad" dependencies = [ "http-body-util", - "hyper 1.4.1", + "hyper", "hyper-util", "pin-project-lite", "tokio", @@ -1418,9 +1422,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "hyper 1.4.1", + "http", + "http-body", + "hyper", "pin-project-lite", "socket2 0.5.7", "tokio", @@ -1445,16 +1449,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "inout" version = "0.1.3" @@ -1485,6 +1479,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -1494,6 +1497,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8d6b3f301ba426b30feca834a2a18d48d5b54e5065496b5c1b05537bee3639" +dependencies = [ + "base64 0.13.1", + "serde", + "serde_json", +] + [[package]] name = "jsonrpc" version = "0.18.0" @@ -1518,6 +1532,23 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.5.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.11" @@ -1598,8 +1629,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fdef521c74c2884a4f3570bcdb6d2a77b3c533feb6b27ac2ae72673cc221c64" dependencies = [ "log", + "once_cell", + "rustls 0.21.7", + "rustls-webpki 0.101.6", "serde", "serde_json", + "webpki-roots 0.25.2", ] [[package]] @@ -1658,29 +1693,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ohttp" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578cb11a3fb5c85697ed8bb850d5ad1cbf819d3eea05c2b253cf1d240fbb10c5" -dependencies = [ - "aead 0.4.3", - "aes-gcm 0.9.2", - "byteorder", - "chacha20poly1305 0.8.0", - "hex", - "hkdf 0.11.0", - "hpke", - "lazy_static", - "log", - "rand", - "serde", - "serde_derive", - "sha2 0.9.9", - "thiserror", - "toml", -] - [[package]] name = "ohttp-relay" version = "0.0.8" @@ -1688,10 +1700,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7850c40a0aebcba289d3252c0a45f93cba6ad4b0c46b88a5fc51dba6ddce8632" dependencies = [ "futures", - "http 1.1.0", + "http", "http-body-util", - "hyper 1.4.1", - "hyper-rustls 0.26.0", + "hyper", + "hyper-rustls", "hyper-tungstenite", "hyper-util", "once_cell", @@ -1775,6 +1787,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -1784,16 +1807,15 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "payjoin" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf485245549b366884e295426755ce649d924762f676c1cc00e12e21501884a3" +source = "git+https://github.com/payjoin/rust-payjoin#e7c290fdab072d98f33fae13f358ac1a336e9e47" dependencies = [ "bhttp", "bip21", "bitcoin 0.32.2", - "chacha20poly1305 0.10.1", - "http 1.1.0", + "bitcoin-hpke", + "bitcoin-ohttp", + "http", "log", - "ohttp", "reqwest", "rustls 0.22.4", "serde", @@ -1804,18 +1826,21 @@ dependencies = [ [[package]] name = "payjoin-directory" version = "0.0.1" -source = "git+https://github.com/payjoin/rust-payjoin#12e08ce3476562e5d22512e17c13281e156bc4b2" +source = "git+https://github.com/payjoin/rust-payjoin#e7c290fdab072d98f33fae13f358ac1a336e9e47" dependencies = [ "anyhow", "bhttp", - "bitcoin 0.30.1", + "bitcoin 0.32.2", + "bitcoin-ohttp", "futures", - "hyper 0.14.30", - "hyper-rustls 0.24.2", - "ohttp", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", "redis", - "rustls 0.21.7", + "rustls 0.22.4", "tokio", + "tokio-rustls", "tracing", "tracing-subscriber", ] @@ -1826,10 +1851,11 @@ version = "0.20.0" dependencies = [ "base64 0.22.1", "bdk", + "bitcoin-ohttp", "bitcoincore-rpc", + "bitcoind", "hex", - "http 1.1.0", - "ohttp", + "http", "ohttp-relay", "payjoin", "payjoin-directory", @@ -1844,6 +1870,18 @@ dependencies = [ "url", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.8", +] + [[package]] name = "pem" version = "3.0.4" @@ -1898,6 +1936,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "plain" version = "0.2.3" @@ -1938,18 +1982,6 @@ dependencies = [ "universal-hash 0.4.0", ] -[[package]] -name = "polyval" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.9", - "opaque-debug", - "universal-hash 0.5.1", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -1988,7 +2020,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1998,15 +2030,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - [[package]] name = "rand_core" version = "0.6.4" @@ -2121,11 +2147,11 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.4.1", - "hyper-rustls 0.26.0", + "hyper", + "hyper-rustls", "hyper-util", "ipnet", "js-sys", @@ -2135,14 +2161,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.22.4", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", @@ -2187,6 +2213,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.21.7" @@ -2213,18 +2252,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - [[package]] name = "rustls-native-certs" version = "0.7.1" @@ -2232,21 +2259,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -2431,11 +2449,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -2633,9 +2652,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -2665,6 +2684,29 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "tar" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "testcontainers" version = "0.15.0" @@ -2794,16 +2836,6 @@ dependencies = [ "syn 2.0.48", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.7", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" @@ -2952,7 +2984,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http", "httparse", "log", "rand", @@ -3350,6 +3382,18 @@ dependencies = [ "nom", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3390,6 +3434,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3522,14 +3575,14 @@ dependencies = [ ] [[package]] -name = "x25519-dalek" -version = "2.0.0-pre.1" +name = "xattr" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "zeroize", + "libc", + "linux-raw-sys", + "rustix", ] [[package]] @@ -3560,3 +3613,52 @@ dependencies = [ "quote", "syn 2.0.48", ] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes 0.8.4", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac 0.12.1", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 6b568da..f4c5208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,8 @@ uniffi = { version = "0.28.0", features = ["build"] } [dev-dependencies] uniffi = { version = "0.28.0", features = ["bindgen-tests"] } -bdk = { version = "0.29.0", features = ["all-keys", "use-esplora-ureq", "keys-bip39"] } +bdk = { version = "0.29.0", features = ["all-keys", "use-esplora-ureq", "keys-bip39", "rpc"] } +bitcoind = { version = "0.36.0", features = ["0_21_2"] } bitcoincore-rpc = "0.19.0" http = "1" payjoin-directory = { git = "https://github.com/payjoin/rust-payjoin", features = ["danger-local-https"] } @@ -27,10 +28,10 @@ testcontainers-modules = { version = "0.1.3", features = ["redis"] } tokio = { version = "1.12.0", features = ["full"] } [dependencies] -payjoin = {version = "=0.20.0", features = ["send", "receive", "base64", "v2", "io"] } +payjoin = { git = "https://github.com/payjoin/rust-payjoin", features = ["send", "receive", "base64", "v2", "io"] } uniffi = { version = "0.28.0" } thiserror = "1.0.47" -ohttp = { version = "0.5.1" } +ohttp = { package = "bitcoin-ohttp", version = "0.6.0" } url = "2.5.0" base64 = "0.22.1" hex = "0.4.3" diff --git a/README.md b/README.md index 3d83ca1..5a812f1 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ The integration tests illustrates and verify integration using bitcoin core and ```shell # Run the integration test -cargo test --package payjoin_ffi --test bitcoin_core_integration_test v1_to_v1_full_cycle +cargo test --package payjoin_ffi --test bitcoin_core_integration v1_to_v1_full_cycle cargo test --package payjoin_ffi --test bdk_integration_test v1_to_v1_full_cycle cargo test --package payjoin_ffi --test bdk_integration_test v2_to_v2_full_cycle --features danger-local-https diff --git a/src/bitcoin.rs b/src/bitcoin.rs index a8d46c0..5b9c3d7 100644 --- a/src/bitcoin.rs +++ b/src/bitcoin.rs @@ -26,6 +26,61 @@ impl From for OutPoint { } } +#[derive(Debug, Clone)] +pub struct PsbtInput { + pub witness_utxo: Option, + pub redeem_script: Option, + pub witness_script: Option, +} + +impl PsbtInput { + pub fn new( + witness_utxo: Option, + redeem_script: Option, + witness_script: Option, + ) -> Self { + Self { witness_utxo, redeem_script, witness_script } + } +} + +impl From for PsbtInput { + fn from(psbt_input: bitcoin::psbt::Input) -> Self { + Self { + witness_utxo: psbt_input.witness_utxo.map(|s| s.into()), + redeem_script: psbt_input.redeem_script.clone().map(|s| s.into()), + witness_script: psbt_input.witness_script.clone().map(|s| s.into()), + } + } +} + +impl From for bitcoin::psbt::Input { + fn from(psbt_input: PsbtInput) -> Self { + Self { + witness_utxo: psbt_input.witness_utxo.map(|s| s.into()), + redeem_script: psbt_input.redeem_script.map(|s| s.into()), + witness_script: psbt_input.witness_script.map(|s| s.into()), + ..Default::default() + } + } +} + +#[derive(Debug, Clone)] +pub struct TxIn { + pub previous_output: OutPoint, +} + +impl From for bitcoin::TxIn { + fn from(tx_in: TxIn) -> Self { + bitcoin::TxIn { previous_output: tx_in.previous_output.into(), ..Default::default() } + } +} + +impl From for TxIn { + fn from(tx_in: bitcoin::TxIn) -> Self { + TxIn { previous_output: tx_in.previous_output.into() } + } +} + #[derive(Debug, Clone)] pub struct TxOut { /// The value of the output, in satoshis. @@ -72,3 +127,18 @@ impl From for bitcoin::Network { } } } + +#[derive(Clone, Debug)] +pub struct ScriptBuf(pub payjoin::bitcoin::ScriptBuf); + +impl From for ScriptBuf { + fn from(value: payjoin::bitcoin::ScriptBuf) -> Self { + Self(value) + } +} + +impl From for payjoin::bitcoin::ScriptBuf { + fn from(value: ScriptBuf) -> Self { + value.0 + } +} diff --git a/src/error.rs b/src/error.rs index 5190580..1d11677 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,9 @@ use std::fmt::Debug; use payjoin::bitcoin::psbt::PsbtParseError; -use payjoin::receive::{RequestError, SelectionError}; +use payjoin::receive::{ + InputContributionError, OutputSubstitutionError, PsbtInputError, RequestError, SelectionError, +}; use payjoin::send::{CreateRequestError, ResponseError as PdkResponseError, ValidationError}; #[derive(Debug, PartialEq, Eq, thiserror::Error)] @@ -15,7 +17,7 @@ pub enum PayjoinError { #[error("Error encountered while decoding PSBT: {message} ")] PsbtParseError { message: String }, - #[error("Response error: {message}")] + #[error("Response error: {message:?}")] ResponseError { message: String }, ///Error that may occur when the request from sender is malformed. @@ -62,6 +64,15 @@ pub enum PayjoinError { #[error("{message}")] IoError { message: String }, + + #[error("{message}")] + OutputSubstitutionError { message: String }, + + #[error("{message}")] + InputContributionError { message: String }, + + #[error("{message}")] + InputPairError { message: String }, } macro_rules! impl_from_error { @@ -82,10 +93,18 @@ impl_from_error! { payjoin::bitcoin::consensus::encode::Error => TransactionError, payjoin::bitcoin::address::ParseError => InvalidAddress, RequestError => RequestError, - PdkResponseError => ResponseError, ValidationError => ValidationError, CreateRequestError => CreateRequestError, uniffi::UnexpectedUniFFICallbackError => UnexpectedError, + OutputSubstitutionError => OutputSubstitutionError, + InputContributionError => InputContributionError, + PsbtInputError => InputPairError, +} + +impl From for PayjoinError { + fn from(value: PdkResponseError) -> Self { + PayjoinError::ResponseError { message: format!("{:?}", value) } + } } impl From for PayjoinError { diff --git a/src/lib.rs b/src/lib.rs index e81b95f..4df2f8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ pub mod uri; pub use crate::bitcoin::*; use crate::error::PayjoinError; -pub use crate::ohttp::OhttpKeys; +pub use crate::ohttp::*; pub use crate::request::Request; pub use crate::uri::{PjUri, PjUriBuilder, Uri, Url}; diff --git a/src/ohttp.rs b/src/ohttp.rs index cdc2d15..31b4caf 100644 --- a/src/ohttp.rs +++ b/src/ohttp.rs @@ -18,3 +18,20 @@ impl OhttpKeys { payjoin::OhttpKeys::decode(bytes.as_slice()).map(|e| e.into()).map_err(|e| e.into()) } } + +use std::sync::Mutex; + +pub struct ClientResponse(Mutex>); + +impl From<&ClientResponse> for ohttp::ClientResponse { + fn from(value: &ClientResponse) -> Self { + let mut data_guard = value.0.lock().unwrap(); + Option::take(&mut *data_guard).expect("ClientResponse moved out of memory") + } +} + +impl From for ClientResponse { + fn from(value: ohttp::ClientResponse) -> Self { + Self(Mutex::new(Some(value))) + } +} diff --git a/src/receive/v1.rs b/src/receive/v1.rs index a6bcc23..d03c5d1 100644 --- a/src/receive/v1.rs +++ b/src/receive/v1.rs @@ -6,7 +6,8 @@ use payjoin::bitcoin::psbt::Psbt; use payjoin::bitcoin::FeeRate; use payjoin::receive as pdk; -use crate::bitcoin::{OutPoint, TxOut}; +use super::v2::InputPair; +use crate::bitcoin::{OutPoint, ScriptBuf, TxOut}; use crate::error::PayjoinError; pub trait CanBroadcast { @@ -107,7 +108,7 @@ impl UncheckedProposal { self.0 .clone() .check_broadcast_suitability( - min_fee_rate.map(|x| FeeRate::from_sat_per_kwu(x)), + min_fee_rate.map(FeeRate::from_sat_per_kwu), |transaction| { can_broadcast(&payjoin::bitcoin::consensus::encode::serialize(transaction)) .map_err(|e| payjoin::receive::Error::Server(Box::new(e))) @@ -147,7 +148,7 @@ impl MaybeInputsOwned { pub fn check_inputs_not_owned( &self, is_owned: Box, - ) -> Result, PayjoinError> { + ) -> Result, PayjoinError> { self.0 .clone() .check_inputs_not_owned(|input| { @@ -162,7 +163,7 @@ impl MaybeInputsOwned { pub fn check_inputs_not_owned( &self, is_owned: impl Fn(&Vec) -> Result, - ) -> Result, PayjoinError> { + ) -> Result, PayjoinError> { self.0 .clone() .check_inputs_not_owned(|input| { @@ -174,31 +175,6 @@ impl MaybeInputsOwned { } } -/// Typestate to validate that the Original PSBT has no inputs that have been seen before. -/// -/// Call check_no_inputs_seen to proceed. -#[derive(Clone)] -pub struct MaybeMixedInputScripts(pdk::MaybeMixedInputScripts); - -impl From for MaybeMixedInputScripts { - fn from(value: pdk::MaybeMixedInputScripts) -> Self { - Self(value) - } -} - -impl MaybeMixedInputScripts { - /// Verify the original transaction did not have mixed input types Call this after checking downstream. - /// - /// Note: mixed spends do not necessarily indicate distinct wallet fingerprints. This check is intended to prevent some types of wallet fingerprinting. - pub fn check_no_mixed_input_scripts(&self) -> Result, PayjoinError> { - self.0 - .clone() - .check_no_mixed_input_scripts() - .map(|e| Arc::new(e.into())) - .map_err(|e| e.into()) - } -} - pub trait IsOutputKnown { fn callback(&self, outpoint: OutPoint) -> Result; } @@ -240,7 +216,7 @@ impl MaybeInputsSeen { self.0 .clone() .check_no_inputs_seen_before(|outpoint| { - is_known(&outpoint.clone().into()).map_err(|e| pdk::Error::Server(Box::new(e))) + is_known(&(*outpoint).into()).map_err(|e| pdk::Error::Server(Box::new(e))) }) .map_err(|e| e.into()) .map(|e| Arc::new(e.into())) @@ -280,7 +256,7 @@ impl OutputsUnknown { pub fn identify_receiver_outputs( &self, is_receiver_output: impl Fn(&Vec) -> Result, - ) -> Result { + ) -> Result { self.0 .clone() .identify_receiver_outputs(|input| { @@ -292,58 +268,64 @@ impl OutputsUnknown { } } -///A mutable checked proposal that the receiver may contribute inputs to make a payjoin. -pub struct ProvisionalProposal(Mutex); +pub struct WantsOutputs(payjoin::receive::WantsOutputs); -impl From for ProvisionalProposal { - fn from(value: pdk::ProvisionalProposal) -> Self { - Self(Mutex::new(value)) +impl From for WantsOutputs { + fn from(value: payjoin::receive::WantsOutputs) -> Self { + Self(value) } } - -pub trait ProcessPartiallySignedTransaction { - fn callback(&self, psbt: String) -> Result; -} - -impl ProvisionalProposal { - fn mutex_guard(&self) -> MutexGuard<'_, payjoin::receive::ProvisionalProposal> { - self.0.lock().unwrap() - } - #[cfg(not(feature = "uniffi"))] - ///If output substitution is enabled, replace the receiver’s output script with a new one. - pub fn try_substitute_receiver_output( +impl WantsOutputs { + pub fn replace_receiver_outputs( &self, - generate_script: impl Fn() -> Result, PayjoinError>, - ) -> Result<(), PayjoinError> { - self.mutex_guard() - .try_substitute_receiver_output(|| { - generate_script() - .map(|e| payjoin::bitcoin::ScriptBuf::from_bytes(e)) - .map_err(|e| payjoin::Error::Server(Box::new(e))) - }) + replacement_outputs: Vec, + drain_script: &ScriptBuf, + ) -> Result { + let replacement_outputs: Vec = + replacement_outputs.into_iter().map(|t| t.into()).collect(); + self.0 + .clone() + .replace_receiver_outputs(replacement_outputs, &drain_script.0) + .map(|t| t.into()) .map_err(|e| e.into()) } - #[cfg(feature = "uniffi")] - pub fn try_substitute_receiver_output( + + pub fn substitute_receiver_script( &self, - generate_script: Box, - ) -> Result<(), PayjoinError> { - self.mutex_guard() - .try_substitute_receiver_output(|| { - generate_script - .callback() - .map(|e| payjoin::bitcoin::ScriptBuf::from_bytes(e)) - .map_err(|e| payjoin::Error::Server(Box::new(e))) - }) - .map_err(|e| e.into()) + output_script: Vec, + ) -> Result { + self.0 + .clone() + .substitute_receiver_script(&payjoin::bitcoin::ScriptBuf::from_bytes(output_script)) + .map(Into::into) + .map_err(Into::into) } - pub fn contribute_witness_input( + + pub fn commit_outputs(&self) -> WantsInputs { + self.0.clone().commit_outputs().into() + } +} + +pub struct WantsInputs(payjoin::receive::WantsInputs); + +impl From for WantsInputs { + fn from(value: payjoin::receive::WantsInputs) -> Self { + Self(value) + } +} + +impl WantsInputs { + pub fn contribute_inputs( &self, - txo: TxOut, - outpoint: OutPoint, - ) -> Result<(), PayjoinError> { - let txo: payjoin::bitcoin::blockdata::transaction::TxOut = txo.into(); - Ok(self.mutex_guard().contribute_witness_input(txo, outpoint.into())) + replacement_inputs: Vec, + ) -> Result { + let replacement_inputs: Vec = + replacement_inputs.into_iter().map(|t| t.into()).collect(); + self.0.clone().contribute_inputs(replacement_inputs).map(|t| t.into()).map_err(|e| e.into()) + } + + pub fn commit_inputs(self) -> ProvisionalProposal { + self.0.clone().commit_inputs().into() } /// Select receiver input such that the payjoin avoids surveillance. Return the input chosen that has been applied to the Proposal. @@ -353,23 +335,42 @@ impl ProvisionalProposal { /// UIH “Unnecessary input heuristic” is one class of them to avoid. We define UIH1 and UIH2 according to the BlockSci practice BlockSci UIH1 and UIH2: pub fn try_preserving_privacy( &self, - candidate_inputs: HashMap, - ) -> Result { - let candidate_inputs: HashMap = - candidate_inputs - .into_iter() - .map(|(key, value)| (payjoin::bitcoin::Amount::from_sat(key), value.into())) - .collect(); - self.mutex_guard() + candidate_inputs: Vec, + ) -> Result { + let candidate_inputs: Vec = + candidate_inputs.into_iter().map(|pair| pair.into()).collect(); + self.0 + .clone() .try_preserving_privacy(candidate_inputs) - .map_err(|e| PayjoinError::SelectionError { message: format!("{:?}", e) }) .map(|o| o.into()) + .map_err(|e| e.into()) } +} + +///A mutable checked proposal that the receiver may contribute inputs to make a payjoin. +pub struct ProvisionalProposal(Mutex); + +impl From for ProvisionalProposal { + fn from(value: pdk::ProvisionalProposal) -> Self { + Self(Mutex::new(value)) + } +} + +pub trait ProcessPartiallySignedTransaction { + fn callback(&self, psbt: String) -> Result; +} + +impl ProvisionalProposal { + fn mutex_guard(&self) -> MutexGuard<'_, payjoin::receive::ProvisionalProposal> { + self.0.lock().unwrap() + } + #[cfg(feature = "uniffi")] pub fn finalize_proposal( &self, process_psbt: Box, min_feerate_sat_per_vb: Option, + max_feerate_sat_per_vb: u64, ) -> Result, PayjoinError> { self.mutex_guard() .clone() @@ -381,6 +382,7 @@ impl ProvisionalProposal { .map_err(|e| pdk::Error::Server(Box::new(e))) }, min_feerate_sat_per_vb.and_then(|x| FeeRate::from_sat_per_vb(x)), + FeeRate::from_sat_per_vb(max_feerate_sat_per_vb), ) .map(|e| Arc::new(e.into())) .map_err(|e| e.into()) @@ -390,6 +392,7 @@ impl ProvisionalProposal { &self, process_psbt: impl Fn(String) -> Result, min_feerate_sat_per_vb: Option, + max_feerate_sat_per_vb: u64, ) -> Result, PayjoinError> { self.mutex_guard() .clone() @@ -399,7 +402,9 @@ impl ProvisionalProposal { .map(|e| Psbt::from_str(e.as_str()).expect("Invalid process_psbt ")) .map_err(|e| pdk::Error::Server(Box::new(e))) }, - min_feerate_sat_per_vb.and_then(|x| FeeRate::from_sat_per_vb(x)), + min_feerate_sat_per_vb.and_then(FeeRate::from_sat_per_vb), + FeeRate::from_sat_per_vb(max_feerate_sat_per_vb) + .expect("max_feerate_sat_per_vb is too high"), ) .map(|e| Arc::new(e.into())) .map_err(|e| e.into()) @@ -432,9 +437,7 @@ impl PayjoinProposal { pub fn is_output_substitution_disabled(&self) -> bool { self.0.is_output_substitution_disabled() } - pub fn owned_vouts(&self) -> Vec { - self.0.owned_vouts().iter().map(|x| *x as u64).collect() - } + pub fn psbt(&self) -> String { self.0.psbt().to_string() } @@ -477,8 +480,6 @@ mod test { .clone() .check_inputs_not_owned(|_| Ok(true)) .expect("No inputs should be owned") - .check_no_mixed_input_scripts() - .expect("No mixed input scripts") .check_no_inputs_seen_before(|_| Ok(false)) .expect("No inputs should be seen before") .identify_receiver_outputs(|script| { diff --git a/src/receive/v2.rs b/src/receive/v2.rs index 48e4ec5..05f2313 100644 --- a/src/receive/v2.rs +++ b/src/receive/v2.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::io::Cursor; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; @@ -8,29 +7,14 @@ use payjoin::bitcoin::psbt::Psbt; use payjoin::bitcoin::FeeRate; use payjoin::receive as pdk; -use crate::bitcoin::Network; -use crate::ohttp::OhttpKeys; +use crate::bitcoin::{Network, ScriptBuf}; +use crate::ohttp::{ClientResponse, OhttpKeys}; #[cfg(feature = "uniffi")] use crate::receive::v1::{ CanBroadcast, GenerateScript, IsOutputKnown, IsScriptOwned, ProcessPartiallySignedTransaction, }; -use crate::request::Request; -use crate::uri::{PjUriBuilder, Url}; -use crate::{OutPoint, PayjoinError, TxOut}; - -pub struct ClientResponse(Mutex>); - -impl From<&ClientResponse> for ohttp::ClientResponse { - fn from(value: &ClientResponse) -> Self { - let mut data_guard = value.0.lock().unwrap(); - Option::take(&mut *data_guard).expect("ClientResponse moved out of memory") - } -} -impl From for ClientResponse { - fn from(value: ohttp::ClientResponse) -> Self { - Self(Mutex::new(Some(value))) - } -} +use crate::uri::PjUriBuilder; +use crate::{OutPoint, PayjoinError, Request, TxOut, Url}; pub struct RequestResponse { pub request: Request, @@ -38,24 +22,25 @@ pub struct RequestResponse { } #[derive(Clone, Debug)] -pub struct SessionInitializer(pub payjoin::receive::v2::SessionInitializer); -impl From for payjoin::receive::v2::SessionInitializer { - fn from(value: SessionInitializer) -> Self { +pub struct Receiver(pub payjoin::receive::v2::Receiver); +impl From for payjoin::receive::v2::Receiver { + fn from(value: Receiver) -> Self { value.0 } } -impl From for SessionInitializer { - fn from(value: payjoin::receive::v2::SessionInitializer) -> Self { +impl From for Receiver { + fn from(value: payjoin::receive::v2::Receiver) -> Self { Self(value) } } -impl SessionInitializer { +impl Receiver { /// Creates a new `SessionInitializer` with the provided parameters. /// /// # Parameters /// - `address`: The Bitcoin address for the payjoin session. + /// - `network`: The network to use for address verification. /// - `directory`: The URL of the store-and-forward payjoin directory. /// - `ohttp_keys`: The OHTTP keys used for encrypting and decrypting HTTP requests and responses. /// - `ohttp_relay`: The URL of the OHTTP relay, used to keep client IP address confidential. @@ -69,15 +54,15 @@ impl SessionInitializer { #[cfg(feature = "uniffi")] pub fn new( address: String, - expire_after: Option, network: Network, directory: Arc, ohttp_keys: Arc, ohttp_relay: Arc, + expire_after: Option, ) -> Result { let address = payjoin::bitcoin::Address::from_str(address.as_str())? .require_network(network.into())?; - Ok(payjoin::receive::v2::SessionInitializer::new( + Ok(payjoin::receive::v2::Receiver::new( address, (*directory).clone().into(), (*ohttp_keys).clone().into(), @@ -89,20 +74,20 @@ impl SessionInitializer { #[cfg(not(feature = "uniffi"))] pub fn new( address: String, - expire_after: Option, network: Network, directory: Url, ohttp_keys: OhttpKeys, ohttp_relay: Url, + expire_after: Option, ) -> Result { let address = payjoin::bitcoin::Address::from_str(address.as_str())? .require_network(network.into())?; - Ok(payjoin::receive::v2::SessionInitializer::new( + Ok(payjoin::receive::v2::Receiver::new( address, directory.into(), ohttp_keys.into(), ohttp_relay.into(), - expire_after.map(|e| Duration::from_secs(e)), + expire_after.map(Duration::from_secs), ) .into()) } @@ -123,61 +108,7 @@ impl SessionInitializer { Err(e) => Err(PayjoinError::V2Error { message: e.to_string() }), } } - #[cfg(not(feature = "uniffi"))] - pub fn process_res( - &self, - body: Vec, - ctx: ohttp::ClientResponse, - ) -> Result { - >::into(self.clone()) - .process_res(Cursor::new(body), ctx) - .map(|e| e.into()) - .map_err(|e| e.into()) - } - #[cfg(feature = "uniffi")] - pub fn process_res( - &self, - body: Vec, - ctx: Arc, - ) -> Result, PayjoinError> { - >::into(self.clone()) - .process_res(Cursor::new(body), ctx.as_ref().into()) - .map(|e| Arc::new(e.into())) - .map_err(|e| e.into()) - } -} -#[derive(Clone, Debug)] -pub struct ActiveSession(payjoin::receive::v2::ActiveSession); - -impl From for payjoin::receive::v2::ActiveSession { - fn from(value: ActiveSession) -> Self { - value.0 - } -} -impl From for ActiveSession { - fn from(value: payjoin::receive::v2::ActiveSession) -> Self { - Self(value) - } -} -impl ActiveSession { - #[cfg(feature = "uniffi")] - pub fn extract_req(&self) -> Result { - match self.0.clone().extract_req() { - Ok(e) => { - Ok(RequestResponse { request: e.0.into(), client_response: Arc::new(e.1.into()) }) - } - Err(e) => Err(PayjoinError::V2Error { message: e.to_string() }), - } - } - - #[cfg(not(feature = "uniffi"))] - pub fn extract_req(&self) -> Result<(Request, ohttp::ClientResponse), PayjoinError> { - match self.0.clone().extract_req() { - Ok(e) => Ok((e.0.into(), e.1)), - Err(e) => Err(PayjoinError::V2Error { message: e.to_string() }), - } - } ///The response can either be an UncheckedProposal or an ACCEPTED message indicating no UncheckedProposal is available yet. #[cfg(feature = "uniffi")] pub fn process_res( @@ -185,7 +116,7 @@ impl ActiveSession { body: Vec, context: Arc, ) -> Result>, PayjoinError> { - >::into(self.clone()) + >::into(self.clone()) .process_res(Cursor::new(body), context.as_ref().into()) .map(|e| e.map(|x| Arc::new(x.into()))) .map_err(|e| e.into()) @@ -197,21 +128,20 @@ impl ActiveSession { body: Vec, ctx: ohttp::ClientResponse, ) -> Result, PayjoinError> { - >::into(self.clone()) + >::into(self.clone()) .process_res(Cursor::new(body), ctx) .map(|e| e.map(|o| o.into())) .map_err(|e| e.into()) } + #[cfg(not(feature = "uniffi"))] pub fn pj_uri_builder(&self) -> PjUriBuilder { - >::into(self.clone()) - .pj_uri_builder() - .into() + >::into(self.clone()).pj_uri_builder().into() } #[cfg(feature = "uniffi")] pub fn pj_uri_builder(&self) -> Arc { Arc::new( - >::into(self.clone()) + >::into(self.clone()) .pj_uri_builder() .into(), ) @@ -220,23 +150,15 @@ impl ActiveSession { /// This identifies a session at the payjoin directory server. #[cfg(feature = "uniffi")] pub fn pj_url(&self) -> Arc { - Arc::new( - >::into(self.clone()) - .pj_url() - .into(), - ) + Arc::new(>::into(self.clone()).pj_url().into()) } #[cfg(not(feature = "uniffi"))] pub fn pj_url(&self) -> Url { - >::into(self.clone()) - .pj_url() - .into() + >::into(self.clone()).pj_url().into() } ///The per-session public key to use as an identifier - pub fn public_key(&self) -> String { - >::into(self.clone()) - .public_key() - .to_string() + pub fn id(&self) -> Vec { + >::into(self.clone()).id().to_vec() } } @@ -302,7 +224,7 @@ impl V2UncheckedProposal { self.0 .clone() .check_broadcast_suitability( - min_fee_rate.map(|x| FeeRate::from_sat_per_kwu(x)), + min_fee_rate.map(FeeRate::from_sat_per_kwu), |transaction| { can_broadcast(&payjoin::bitcoin::consensus::encode::serialize(transaction)) .map_err(|e| payjoin::receive::Error::Server(Box::new(e))) @@ -335,7 +257,7 @@ impl V2MaybeInputsOwned { pub fn check_inputs_not_owned( &self, is_owned: Box, - ) -> Result, PayjoinError> { + ) -> Result, PayjoinError> { self.0 .clone() .check_inputs_not_owned(|input| { @@ -350,7 +272,7 @@ impl V2MaybeInputsOwned { pub fn check_inputs_not_owned( &self, is_owned: impl Fn(&Vec) -> Result, - ) -> Result, PayjoinError> { + ) -> Result, PayjoinError> { self.0 .clone() .check_inputs_not_owned(|input| { @@ -361,29 +283,7 @@ impl V2MaybeInputsOwned { .map(|e| Arc::new(e.into())) } } -#[derive(Clone)] -pub struct V2MaybeMixedInputScripts(payjoin::receive::v2::MaybeMixedInputScripts); -impl From for V2MaybeMixedInputScripts { - fn from(value: payjoin::receive::v2::MaybeMixedInputScripts) -> Self { - Self(value) - } -} - -impl V2MaybeMixedInputScripts { - /// Verify the original transaction did not have mixed input types - /// Call this after checking downstream. - /// - /// Note: mixed spends do not necessarily indicate distinct wallet fingerprints. - /// This check is intended to prevent some types of wallet fingerprinting. - pub fn check_no_mixed_input_scripts(&self) -> Result, PayjoinError> { - self.0 - .clone() - .check_no_mixed_input_scripts() - .map(|e| Arc::new(e.into())) - .map_err(|e| e.into()) - } -} #[derive(Clone)] pub struct V2MaybeInputsSeen(payjoin::receive::v2::MaybeInputsSeen); impl From for V2MaybeInputsSeen { @@ -419,7 +319,7 @@ impl V2MaybeInputsSeen { self.0 .clone() .check_no_inputs_seen_before(|outpoint| { - is_known(&outpoint.clone().into()).map_err(|e| pdk::Error::Server(Box::new(e))) + is_known(&(*outpoint).into()).map_err(|e| pdk::Error::Server(Box::new(e))) }) .map_err(|e| e.into()) .map(|e| Arc::new(e.into())) @@ -445,7 +345,7 @@ impl V2OutputsUnknown { pub fn identify_receiver_outputs( &self, is_receiver_output: Box, - ) -> Result, PayjoinError> { + ) -> Result, PayjoinError> { self.0 .clone() .identify_receiver_outputs(|output_script| { @@ -460,7 +360,7 @@ impl V2OutputsUnknown { pub fn identify_receiver_outputs( &self, is_receiver_output: impl Fn(&Vec) -> Result, - ) -> Result { + ) -> Result { self.0 .clone() .identify_receiver_outputs(|input| { @@ -472,28 +372,90 @@ impl V2OutputsUnknown { } } -pub struct V2ProvisionalProposal(pub Mutex); +pub struct V2WantsOutputs(payjoin::receive::v2::WantsOutputs); -impl From for V2ProvisionalProposal { - fn from(value: payjoin::receive::v2::ProvisionalProposal) -> Self { - Self(Mutex::new(value)) +impl From for V2WantsOutputs { + fn from(value: payjoin::receive::v2::WantsOutputs) -> Self { + Self(value) } } +impl V2WantsOutputs { + // fn mutex_guard(&self) -> MutexGuard<'_, payjoin::receive::v2::WantsOutputs> { + // self.0.lock().unwrap() + // } -/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. -impl V2ProvisionalProposal { - fn mutex_guard(&self) -> MutexGuard<'_, payjoin::receive::v2::ProvisionalProposal> { - self.0.lock().unwrap() + pub fn is_output_substitution_disabled(&self) -> bool { + self.0.is_output_substitution_disabled() } - pub fn contribute_witness_input( + pub fn replace_receiver_outputs( &self, - txo: TxOut, - outpoint: OutPoint, - ) -> Result<(), PayjoinError> { - let txo: payjoin::bitcoin::blockdata::transaction::TxOut = txo.into(); - Ok(self.mutex_guard().contribute_witness_input(txo, outpoint.into())) + replacement_outputs: Vec, + drain_script: &ScriptBuf, + ) -> Result { + let replacement_outputs: Vec = + replacement_outputs.iter().map(|o| o.clone().into()).collect(); + self.0 + .clone() + .replace_receiver_outputs(replacement_outputs, &drain_script.0) + .map(|e| e.into()) + .map_err(|e| e.into()) + } + + pub fn commit_outputs(&self) -> V2WantsInputs { + self.0.clone().commit_outputs().into() } + + pub fn substitute_receiver_script( + &self, + output_script: Vec, + ) -> Result { + self.0 + .clone() + .substitute_receiver_script(&payjoin::bitcoin::ScriptBuf::from_bytes(output_script)) + .map(Into::into) + .map_err(Into::into) + } + + // #[cfg(not(feature = "uniffi"))] + // ///If output substitution is enabled, replace the receiver’s output script with a new one. + // pub fn try_substitute_receiver_output( + // &self, + // generate_script: impl Fn() -> Result, PayjoinError>, + // ) -> Result<(), PayjoinError> { + // self.mutex_guard() + // .try_substitute_receiver_output(|| { + // generate_script() + // .map(|e| payjoin::bitcoin::ScriptBuf::from_bytes(e)) + // .map_err(|e| payjoin::Error::Server(Box::new(e))) + // }) + // .map_err(|e| e.into()) + // } + // #[cfg(feature = "uniffi")] + // pub fn try_substitute_receiver_output( + // &self, + // generate_script: Box, + // ) -> Result<(), PayjoinError> { + // self.mutex_guard() + // .try_substitute_receiver_output(|| { + // generate_script + // .callback() + // .map(|e| payjoin::bitcoin::ScriptBuf::from_bytes(e)) + // .map_err(|e| payjoin::Error::Server(Box::new(e))) + // }) + // .map_err(|e| e.into()) + // } +} + +pub struct V2WantsInputs(payjoin::receive::v2::WantsInputs); + +impl From for V2WantsInputs { + fn from(value: payjoin::receive::v2::WantsInputs) -> Self { + Self(value) + } +} + +impl V2WantsInputs { /// Select receiver input such that the payjoin avoids surveillance. /// Return the input chosen that has been applied to the Proposal. /// @@ -507,50 +469,67 @@ impl V2ProvisionalProposal { // https://eprint.iacr.org/2022/589.pdf pub fn try_preserving_privacy( &self, - candidate_inputs: HashMap, - ) -> Result { - let candidate_inputs: HashMap = - candidate_inputs - .into_iter() - .map(|(key, value)| (payjoin::bitcoin::Amount::from_sat(key), value.into())) - .collect(); - - match self.mutex_guard().try_preserving_privacy(candidate_inputs) { - Ok(e) => Ok(OutPoint { txid: e.txid.to_string(), vout: e.vout }), + candidate_inputs: Vec, + ) -> Result { + let candidate_inputs: Vec = + candidate_inputs.into_iter().map(|pair| pair.into()).collect(); + + match self.0.clone().try_preserving_privacy(candidate_inputs) { + Ok(e) => Ok(e.into()), Err(e) => Err(e.into()), } } - pub fn is_output_substitution_disabled(&self) -> bool { - self.mutex_guard().is_output_substitution_disabled() - } - #[cfg(not(feature = "uniffi"))] - ///If output substitution is enabled, replace the receiver’s output script with a new one. - pub fn try_substitute_receiver_output( + pub fn contribute_inputs( &self, - generate_script: impl Fn() -> Result, PayjoinError>, - ) -> Result<(), PayjoinError> { - self.mutex_guard() - .try_substitute_receiver_output(|| { - generate_script() - .map(|e| payjoin::bitcoin::ScriptBuf::from_bytes(e)) - .map_err(|e| payjoin::Error::Server(Box::new(e))) - }) - .map_err(|e| e.into()) + replacement_inputs: Vec, + ) -> Result { + let replacement_inputs: Vec = + replacement_inputs.into_iter().map(|pair| pair.into()).collect(); + self.0.clone().contribute_inputs(replacement_inputs).map(|t| t.into()).map_err(|e| e.into()) } - #[cfg(feature = "uniffi")] - pub fn try_substitute_receiver_output( - &self, - generate_script: Box, - ) -> Result<(), PayjoinError> { - self.mutex_guard() - .try_substitute_receiver_output(|| { - generate_script - .callback() - .map(|e| payjoin::bitcoin::ScriptBuf::from_bytes(e)) - .map_err(|e| payjoin::Error::Server(Box::new(e))) - }) - .map_err(|e| e.into()) + + pub fn commit_inputs(self) -> V2ProvisionalProposal { + self.0.clone().commit_inputs().into() + } +} + +#[derive(Debug)] +pub struct InputPair(payjoin::receive::InputPair); + +impl InputPair { + pub fn new( + txin: crate::bitcoin::TxIn, + psbtin: crate::bitcoin::PsbtInput, + ) -> Result { + Ok(Self(payjoin::receive::InputPair::new(txin.into(), psbtin.into())?)) + } +} + +impl From for payjoin::receive::InputPair { + fn from(value: InputPair) -> Self { + value.0 + } +} + +impl From for InputPair { + fn from(value: payjoin::receive::InputPair) -> Self { + Self(value) + } +} + +pub struct V2ProvisionalProposal(pub Mutex); + +impl From for V2ProvisionalProposal { + fn from(value: payjoin::receive::v2::ProvisionalProposal) -> Self { + Self(Mutex::new(value)) + } +} + +/// A mutable checked proposal that the receiver may contribute inputs to to make a payjoin. +impl V2ProvisionalProposal { + fn mutex_guard(&self) -> MutexGuard<'_, payjoin::receive::v2::ProvisionalProposal> { + self.0.lock().unwrap() } #[cfg(feature = "uniffi")] @@ -558,6 +537,7 @@ impl V2ProvisionalProposal { &self, process_psbt: Box, min_feerate_sat_per_vb: Option, + max_fee_rate_sat_per_vb: u64, ) -> Result, PayjoinError> { self.mutex_guard() .clone() @@ -573,6 +553,7 @@ impl V2ProvisionalProposal { } }, min_feerate_sat_per_vb.and_then(|x| FeeRate::from_sat_per_vb(x)), + FeeRate::from_sat_per_vb(max_fee_rate_sat_per_vb), ) .map(|e| Arc::new(e.into())) .map_err(|e| e.into()) @@ -582,6 +563,7 @@ impl V2ProvisionalProposal { &self, process_psbt: impl Fn(String) -> Result, min_feerate_sat_per_vb: Option, + max_feerate_sat_per_vb: u64, ) -> Result, PayjoinError> { self.mutex_guard() .clone() @@ -595,7 +577,10 @@ impl V2ProvisionalProposal { Err(e) => Err(pdk::Error::Server(Box::new(e))), } }, - min_feerate_sat_per_vb.and_then(|x| FeeRate::from_sat_per_vb(x)), + // TODO handle feerate parsing error OR accept FeeRate in function signature + min_feerate_sat_per_vb.and_then(FeeRate::from_sat_per_vb), + FeeRate::from_sat_per_vb(max_feerate_sat_per_vb) + .expect("max_feerate_sat_per_vb is too high"), ) .map(|e| Arc::new(e.into())) .map_err(|e| e.into()) @@ -632,13 +617,6 @@ impl V2PayjoinProposal { >::into(self.clone()) .is_output_substitution_disabled() } - pub fn owned_vouts(&self) -> Vec { - >::into(self.clone()) - .owned_vouts() - .iter() - .map(|x| *x as u64) - .collect() - } pub fn psbt(&self) -> String { >::into(self.clone()) .psbt() diff --git a/src/request.rs b/src/request.rs index 26f6b38..cfa36ec 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,23 +1,27 @@ -use std::sync::Arc; - use crate::uri::Url; ///Represents data that needs to be transmitted to the receiver. ///You need to send this request over HTTP(S) to the receiver. #[derive(Clone, Debug)] pub struct Request { - ///URL to send the request to. + /// URL to send the request to. + /// + /// This is full URL with scheme etc - you can pass it right to `reqwest` or a similar library. + pub url: Url, + + /// The `Content-Type` header to use for the request. /// - ///This is full URL with scheme etc - you can pass it right to reqwest or a similar library. - pub url: Arc, - ///Bytes to be sent to the receiver. + /// `text/plain` for v1 requests and `message/ohttp-req` for v2 requests. + pub content_type: &'static str, + + /// Bytes to be sent to the receiver. /// - ///This is properly encoded PSBT, already in base64. You only need to make sure Content-Type is text/plain and Content-Length is body.len() (most libraries do the latter automatically). + /// This is properly encoded PSBT payload either in base64 in v1 or an OHTTP encapsulated payload in v2. pub body: Vec, } impl From for Request { fn from(value: payjoin::Request) -> Self { - Self { url: Arc::new(value.url.into()), body: value.body } + Self { url: value.url.into(), content_type: value.content_type, body: value.body } } } diff --git a/src/send/mod.rs b/src/send/mod.rs index ae6adc7..99800c8 100644 --- a/src/send/mod.rs +++ b/src/send/mod.rs @@ -1,2 +1,36 @@ +use v2::V2PostContext; + pub mod v1; pub mod v2; + +pub struct Context(payjoin::send::Context); + +impl From for Context { + fn from(value: payjoin::send::Context) -> Self { + Self(value) + } +} + +impl Context { + pub fn is_v1(&self) -> bool { + matches!(self.0, payjoin::send::Context::V1(_)) + } + + pub fn is_v2(&self) -> bool { + matches!(self.0, payjoin::send::Context::V2(_)) + } + + pub fn as_v1(self) -> Option { + match self.0 { + payjoin::send::Context::V1(ctx) => Some(ctx.into()), + _ => None, + } + } + + pub fn as_v2(self) -> Option { + match self.0 { + payjoin::send::Context::V2(ctx) => Some(ctx.into()), + _ => None, + } + } +} diff --git a/src/send/v1.rs b/src/send/v1.rs index 00490e7..884e110 100644 --- a/src/send/v1.rs +++ b/src/send/v1.rs @@ -6,27 +6,27 @@ pub use payjoin::send as pdk; use crate::error::PayjoinError; use crate::request::Request; -use crate::send::v2::ContextV2; +use crate::send::Context; use crate::uri::{PjUri, Url}; ///Builder for sender-side payjoin parameters /// ///These parameters define how client wants to handle Payjoin. #[derive(Clone)] -pub struct RequestBuilder(pdk::RequestBuilder<'static>); +pub struct SenderBuilder(pdk::SenderBuilder<'static>); -impl From> for RequestBuilder { - fn from(value: pdk::RequestBuilder<'static>) -> Self { +impl From> for SenderBuilder { + fn from(value: pdk::SenderBuilder<'static>) -> Self { Self(value) } } -impl RequestBuilder { +impl SenderBuilder { //TODO: Replicate all functions like this & remove duplicate code /// Prepare an HTTP request and request context to process the response /// /// An HTTP client will own the Request data while Context sticks around so - /// a `(Request, Context)` tuple is returned from `RequestBuilder::build()` + /// a `(Request, Context)` tuple is returned from `SenderBuilder::build()` /// to keep them separated. pub fn from_psbt_and_uri( psbt: String, @@ -36,7 +36,7 @@ impl RequestBuilder { let psbt = payjoin::bitcoin::psbt::Psbt::from_str(psbt.as_str())?; #[cfg(feature = "uniffi")] let uri: PjUri = (*uri).clone(); - pdk::RequestBuilder::from_psbt_and_uri(psbt, uri.into()) + pdk::SenderBuilder::from_psbt_and_uri(psbt, uri.into()) .map(|e| e.into()) .map_err(|e| e.into()) } @@ -56,10 +56,7 @@ impl RequestBuilder { // The minfeerate parameter is set if the contribution is available in change. // // This method fails if no recommendation can be made or if the PSBT is malformed. - pub fn build_recommended( - &self, - min_fee_rate: u64, - ) -> Result, PayjoinError> { + pub fn build_recommended(&self, min_fee_rate: u64) -> Result, PayjoinError> { self.0 .clone() .build_recommended(payjoin::bitcoin::FeeRate::from_sat_per_kwu(min_fee_rate)) @@ -85,7 +82,7 @@ impl RequestBuilder { change_index: Option, min_fee_rate: u64, clamp_fee_contribution: bool, - ) -> Result, PayjoinError> { + ) -> Result, PayjoinError> { self.0 .clone() .build_with_additional_fee( @@ -101,10 +98,7 @@ impl RequestBuilder { /// /// While it's generally better to offer some contribution some users may wish not to. /// This function disables contribution. - pub fn build_non_incentivizing( - &self, - min_fee_rate: u64, - ) -> Result, PayjoinError> { + pub fn build_non_incentivizing(&self, min_fee_rate: u64) -> Result, PayjoinError> { match self .0 .clone() @@ -116,62 +110,68 @@ impl RequestBuilder { } } #[derive(Clone)] -pub struct RequestContext(payjoin::send::RequestContext); +pub struct Sender(payjoin::send::Sender); -impl From for RequestContext { - fn from(value: payjoin::send::RequestContext) -> Self { - RequestContext(value) +impl From for Sender { + fn from(value: payjoin::send::Sender) -> Self { + Self(value) } } -#[derive(Clone)] -pub struct RequestContextV1 { +pub struct RequestContext { pub request: Request, - pub context_v1: Arc, + pub context: Context, } -#[derive(Clone)] -pub struct RequestContextV2 { +pub struct RequestV1Context { pub request: Request, - pub context_v2: Arc, + pub context: V1Context, } -impl RequestContext { - /// Extract serialized V1 Request and Context from a Payjoin Proposal - pub fn extract_v1(&self) -> Result { - match self.0.clone().extract_v1() { - Ok(e) => Ok(RequestContextV1 { request: e.0.into(), context_v1: Arc::new(e.1.into()) }), - Err(e) => Err(e.into()), - } +impl Sender { + pub fn extract_v1(&self) -> Result { + self.0 + .clone() + .extract_v1() + .map(|(req, ctx)| RequestV1Context { request: req.into(), context: ctx.into() }) + .map_err(|e| e.into()) } + /// Extract serialized Request and Context from a Payjoin Proposal. /// /// In order to support polling, this may need to be called many times to be encrypted with /// new unique nonces to make independent OHTTP requests. /// /// The `ohttp_proxy` merely passes the encrypted payload to the ohttp gateway of the receiver - pub fn extract_v2(&self, ohttp_proxy_url: Arc) -> Result { - match self.0.clone().extract_v2((*ohttp_proxy_url).clone().into()) { - Ok(e) => Ok(RequestContextV2 { request: e.0.into(), context_v2: Arc::new(e.1.into()) }), + pub fn extract_highest_version( + &self, + ohttp_proxy_url: Arc, + ) -> Result { + match self.0.clone().extract_highest_version((*ohttp_proxy_url).clone().into()) { + Ok((req, ctx)) => Ok(RequestContext { request: req.into(), context: ctx.into() }), Err(e) => Err(e.into()), } } } + ///Data required for validation of response. -/// This type is used to process the response. Get it from RequestBuilder's build methods. Then you only need to call .process_response() on it to continue BIP78 flow. +/// This type is used to process the response. Get it from SenderBuilder's build methods. Then you only need to call .process_response() on it to continue BIP78 flow. #[derive(Clone)] -pub struct ContextV1(payjoin::send::ContextV1); -impl From for ContextV1 { - fn from(value: payjoin::send::ContextV1) -> Self { - Self(value) +pub struct V1Context(Arc); +impl From for V1Context { + fn from(value: payjoin::send::V1Context) -> Self { + Self(Arc::new(value)) } } -impl ContextV1 { +impl V1Context { ///Decodes and validates the response. /// Call this method with response from receiver to continue BIP78 flow. If the response is valid you will get appropriate PSBT that you should sign and broadcast. pub fn process_response(&self, response: Vec) -> Result { let mut decoder = Cursor::new(response); - self.0.clone().process_response(&mut decoder).map(|e| e.to_string()).map_err(|e| e.into()) + ::clone(&self.0.clone()) + .process_response(&mut decoder) + .map(|e| e.to_string()) + .map_err(|e| e.into()) } } diff --git a/src/send/v2.rs b/src/send/v2.rs index 2641055..0831cd5 100644 --- a/src/send/v2.rs +++ b/src/send/v2.rs @@ -1,29 +1,58 @@ use std::io::Cursor; -use std::sync::Mutex; use crate::error::PayjoinError; +use crate::ohttp::ClientResponse; -pub struct ContextV2(Mutex>); -impl From<&ContextV2> for payjoin::send::ContextV2 { - fn from(value: &ContextV2) -> Self { - let mut data_guard = value.0.lock().unwrap(); - Option::take(&mut *data_guard).expect("ContextV2 moved out of memory") +pub struct V2PostContext(payjoin::send::V2PostContext); + +impl V2PostContext { + /// Decodes and validates the response. + /// Call this method with response from receiver to continue BIP-??? flow. A successful response can either be None if the relay has not response yet or Some(Psbt). + /// If the response is some valid PSBT you should sign and broadcast. + pub fn process_response(self, response: Vec) -> Result { + let mut decoder = Cursor::new(response); + self.0.process_response(&mut decoder).map(|t| t.into()).map_err(|e| e.into()) + } +} + +impl From for V2PostContext { + fn from(value: payjoin::send::V2PostContext) -> Self { + Self(value) } } -impl From for ContextV2 { - fn from(value: payjoin::send::ContextV2) -> Self { - Self(Mutex::new(Some(value))) + +pub struct V2GetContext(payjoin::send::V2GetContext); + +impl From for V2GetContext { + fn from(value: payjoin::send::V2GetContext) -> Self { + Self(value) } } -impl ContextV2 { - ///Decodes and validates the response. + +impl V2GetContext { + pub fn extract_req( + &self, + ohttp_relay: payjoin::Url, + ) -> Result<(crate::Request, crate::ClientResponse), PayjoinError> { + self.0 + .extract_req(ohttp_relay) + .map(|(req, resp)| (req.into(), resp.into())) + .map_err(|e| e.into()) + } + + /// Decodes and validates the response. /// Call this method with response from receiver to continue BIP-??? flow. A successful response can either be None if the relay has not response yet or Some(Psbt). /// If the response is some valid PSBT you should sign and broadcast. - pub fn process_response(&self, response: Vec) -> Result, PayjoinError> { + pub fn process_response( + &self, + response: Vec, + ohttp_ctx: &ClientResponse, + ) -> Result, PayjoinError> { let mut decoder = Cursor::new(response); - <&ContextV2 as Into>::into(self) - .process_response(&mut decoder) - .map(|e| e.map(|o| o.to_string())) - .map_err(|e| e.into()) + match self.0.process_response(&mut decoder, ohttp_ctx.into()) { + Ok(Some(psbt)) => Ok(Some(psbt.to_string())), + Ok(None) => Ok(None), + Err(e) => Err(e.into()), + } } } diff --git a/src/uri.rs b/src/uri.rs index 004a086..23a0baf 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -232,8 +232,8 @@ mod tests { let bech32_upper = "TB1Q6D3A2W975YNY0ASUVD9A67NER4NKS58FF0Q8G4"; let bech32_lower = "tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4"; - for address in vec![base58, bech32_upper, bech32_lower] { - for pj in vec![https, onion] { + for address in [base58, bech32_upper, bech32_lower] { + for pj in [https, onion] { let amount = bitcoin::Amount::ONE_BTC; let builder = PjUriBuilder::new( address.to_string(), diff --git a/tests/bdk_integration_test.rs b/tests/bdk_integration_test.rs index 39e002c..bd31e54 100644 --- a/tests/bdk_integration_test.rs +++ b/tests/bdk_integration_test.rs @@ -1,27 +1,24 @@ -use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; +use bdk::bitcoin::key::Secp256k1; use bdk::bitcoin::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Address, Network, Script, Transaction}; -use bdk::blockchain::EsploraBlockchain; +use bdk::blockchain::{ + Blockchain, ConfigurableBlockchain, EsploraBlockchain, RpcBlockchain, RpcConfig, +}; use bdk::database::MemoryDatabase; +use bdk::descriptor::IntoWalletDescriptor; use bdk::wallet::AddressIndex; use bdk::{FeeRate, LocalUtxo, SignOptions, Wallet as BdkWallet}; -use bitcoincore_rpc::{Auth, Client, RpcApi}; -use payjoin_ffi::bitcoin::{OutPoint, TxOut}; +use bitcoincore_rpc::RpcApi; use payjoin_ffi::error::PayjoinError; use payjoin_ffi::receive::v1::{Headers, PayjoinProposal, UncheckedProposal}; +use payjoin_ffi::receive::v2::InputPair; use payjoin_ffi::uri::{PjUri, Uri}; use payjoin_ffi::Request; use uniffi::deps::log::debug; -// Set up RPC connections -static RPC_USER: &str = "admin1"; -static RPC_PASSWORD: &str = "123"; -static RPC_HOST: &str = "localhost"; -static RPC_PORT: &str = "18443"; -static ESPLORA_URL: &str = "http://0.0.0.0:30000"; type BoxError = Box; pub struct EsploraClient(EsploraBlockchain); @@ -40,6 +37,38 @@ impl EsploraClient { } } +pub struct RpcClient(RpcBlockchain); + +impl RpcClient { + pub fn new(bitcoind: &bitcoind::BitcoinD, descriptor: T) -> Self + where + T: IntoWalletDescriptor, + { + let config = RpcConfig { + url: bitcoind.rpc_url(), + auth: bdk::blockchain::rpc::Auth::Cookie { file: bitcoind.params.cookie_file.clone() }, + network: Network::Regtest, + wallet_name: bdk::wallet::wallet_name_from_descriptor( + descriptor, + None, + Network::Regtest, + &Secp256k1::new(), + ) + .unwrap(), + sync_params: Default::default(), + }; + let client = RpcBlockchain::from_config(&config).unwrap(); + Self(client) + } + + pub fn broadcast(&self, transaction: Transaction) -> Result<(), BoxError> { + match self.0.broadcast(&transaction) { + Ok(_) => Ok(()), + Err(e) => panic!("{}", e.to_string()), + } + } +} + fn restore_wallet(descriptor: String) -> Result { match Wallet::new_no_persist(descriptor.to_string(), Network::Regtest) { Ok(e) => Ok(e), @@ -47,73 +76,50 @@ fn restore_wallet(descriptor: String) -> Result { } } -fn get_bitcoin_client() -> Client { - let url = format!("http://{}:{}/wallet/{}", RPC_HOST, RPC_PORT, ""); - Client::new(&*url, Auth::UserPass(RPC_USER.to_string(), RPC_PASSWORD.to_string())).unwrap() +fn restore_rpc_client(bitcoind: &bitcoind::BitcoinD, descriptor: &str) -> RpcClient { + RpcClient::new(bitcoind, descriptor) } -fn restore_esplora_client() -> EsploraClient { - EsploraClient::new(ESPLORA_URL.to_string()) -} -fn init_sender_receiver_wallet() -> (Wallet, Wallet, Client) { +fn init_sender_receiver_wallet() -> (Wallet, Wallet, bitcoind::BitcoinD) { let sender = restore_wallet(get_sender_descriptor()).expect("Wallet::new failed"); let receiver = restore_wallet(get_receiver_descriptor()).expect("Wallet::new failed"); - let client = get_bitcoin_client(); - let esplora_client = restore_esplora_client(); - let receiver_address = receiver.get_address(AddressIndex::New); - let sender_address = sender.get_address(AddressIndex::New); + let bitcoind_exe = std::env::var("BITCOIND_EXE") + .ok() + .or_else(|| bitcoind::downloaded_exe_path().ok()) + .unwrap(); + let conf = bitcoind::Conf::default(); + let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf) + .expect("bitcoind::BitcoinD::with_conf failed"); + let sender_bdk_rpc_client = restore_rpc_client(&bitcoind, &get_sender_descriptor()); + let receiver_bdk_rpc_client = restore_rpc_client(&bitcoind, &get_receiver_descriptor()); + let receiver_address_bdk = receiver.get_address(AddressIndex::New); + let sender_address_bdk = sender.get_address(AddressIndex::New); + let receiver_address_bitcoind = + bitcoincore_rpc::bitcoin::address::Address::from_str(&*receiver_address_bdk.to_string()) + .unwrap() + .assume_checked(); + let sender_address_bitcoind = + bitcoincore_rpc::bitcoin::address::Address::from_str(&*sender_address_bdk.to_string()) + .unwrap() + .assume_checked(); let sender_balance = sender.get_balance().to_string(); let receiver_balance = receiver.get_balance().to_string(); - client - .send_to_address( - &bitcoincore_rpc::bitcoin::address::Address::from_str(&*receiver_address.to_string()) - .unwrap() - .assume_checked(), - bitcoincore_rpc::bitcoin::Amount::ONE_BTC, - None, - None, - None, - None, - None, - None, - ) - .unwrap(); - client - .send_to_address( - &bitcoincore_rpc::bitcoin::address::Address::from_str(&*sender_address.to_string()) - .unwrap() - .assume_checked(), - bitcoincore_rpc::bitcoin::Amount::ONE_BTC, - None, - None, - None, - None, - None, - None, - ) - .unwrap(); - client - .generate_to_address( - 11, - &bitcoincore_rpc::bitcoin::address::Address::from_str(&*receiver_address.to_string()) - .unwrap() - .assume_checked(), - ) - .expect("generate failed"); - let _ = sender.sync(&esplora_client); - let _ = receiver.sync(&esplora_client); + bitcoind.client.generate_to_address(1, &receiver_address_bitcoind).unwrap(); + bitcoind.client.generate_to_address(101, &sender_address_bitcoind).unwrap(); + let _ = sender.sync(&sender_bdk_rpc_client); + let _ = receiver.sync(&receiver_bdk_rpc_client); println!("\n Sender balance: {:?}", receiver.get_balance()); println!("\n Receiver balance: {:?}", sender.get_balance()); assert_ne!(receiver_balance, receiver.get_balance(), "receiver doesn't own bitcoin"); assert_ne!(sender_balance, sender.get_balance(), "sender doesn't own bitcoin"); - (sender, receiver, client) + (sender, receiver, bitcoind) } #[allow(dead_code)] -fn broadcast_tx(esplora_client: EsploraClient, tx: Transaction) -> Result<(), BoxError> { - esplora_client.broadcast(tx) +fn broadcast_tx(blockchain_client: RpcClient, tx: Transaction) -> Result<(), BoxError> { + blockchain_client.broadcast(tx) } fn build_pj_uri<'a>( address: String, @@ -164,7 +170,7 @@ impl Wallet { pub fn list_unspent(&self) -> Vec { self.get_wallet().list_unspent().unwrap() } - pub fn sync(&self, client: &EsploraClient) { + pub fn sync(&self, client: &RpcClient) { self.get_wallet().sync(&client.0, Default::default()).unwrap(); } fn remove_bip32_derivation_paths(&self, psbt: &mut PartiallySignedTransaction) { @@ -235,11 +241,8 @@ fn handle_proposal(proposal: UncheckedProposal, receiver: Wallet) -> Arc Arc = available_inputs - .iter() - .map(|i| { - (i.txout.value, OutPoint { txid: i.outpoint.txid.to_string(), vout: i.outpoint.vout }) - }) - .collect(); - let selected_outpoint = payjoin.try_preserving_privacy(candidate_inputs).expect("gg"); - let selected_utxo = available_inputs - .iter() - .find(|i| { - i.outpoint.txid.to_string() == selected_outpoint.txid - && i.outpoint.vout == selected_outpoint.vout - }) + let available_inputs = receiver + .list_unspent() + .into_iter() + .map(input_pair_from_local_utxo) + .collect::, _>>() .unwrap(); + let selected_outpoint = wants_inputs.try_preserving_privacy(available_inputs).expect("gg"); + let provisional_proposal = wants_inputs + .contribute_inputs(vec![selected_outpoint]) + .expect("contribute_witness_input error") + .commit_inputs(); - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - let txo_to_contribute = TxOut { - value: selected_utxo.txout.value, - script_pubkey: selected_utxo.txout.script_pubkey.clone().into_bytes(), - }; - let outpoint_to_contribute = OutPoint { - txid: selected_utxo.outpoint.txid.to_string(), - vout: selected_utxo.outpoint.vout, - }; - payjoin - .contribute_witness_input(txo_to_contribute, outpoint_to_contribute) - .expect("contribute_witness_input error"); - - let payjoin_proposal = payjoin + let payjoin_proposal = provisional_proposal .finalize_proposal( |e| { match receiver @@ -290,6 +283,7 @@ fn handle_proposal(proposal: UncheckedProposal, receiver: Wallet) -> Arc Result<(), BoxError> { - let (sender, receiver, _) = init_sender_receiver_wallet(); - let _esplora_client = restore_esplora_client(); + let (sender, receiver, bitcoind) = init_sender_receiver_wallet(); + let _blockchain_client = restore_rpc_client(&bitcoind, &get_sender_descriptor()); let pj_receiver_address = receiver.get_address(AddressIndex::New); @@ -354,25 +348,26 @@ mod v1 { let psbt = build_original_psbt(&sender, &pj_uri)?; println!("\nOriginal sender psbt: {:#?}", psbt.to_string()); - let req_ctx = RequestBuilder::from_psbt_and_uri(psbt.to_string(), pj_uri)? - .build_with_additional_fee(10000, None, 0, false)? + let req_ctx = SenderBuilder::from_psbt_and_uri(psbt.to_string(), pj_uri)? + .build_recommended(payjoin::bitcoin::FeeRate::BROADCAST_MIN.to_sat_per_kwu())? .extract_v1()?; let headers = Headers::from_vec(req_ctx.request.body.clone()); let response = handle_pj_request(req_ctx.request, headers, receiver); - println!("\nOriginal receiver psbt: {:#?}", response); - let checked_payjoin_proposal_psbt = req_ctx - .context_v1 - .process_response(response.as_bytes().to_vec()) - .expect("process res error"); + println!("\nProposal psbt: {:#?}", response); + let checked_payjoin_proposal_psbt = + match req_ctx.context.process_response(response.as_bytes().to_vec()) { + Ok(e) => e, + Err(e) => panic!("process res error: {:#?}", e), + }; + println!("Checked payjoin proposal psbt: {:#?}", checked_payjoin_proposal_psbt); let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt.as_str())?; - _esplora_client.broadcast(payjoin_tx.clone()).expect("Broadcast error"); + _blockchain_client.broadcast(payjoin_tx.clone()).expect("Broadcast error"); println!("Broadcast success: {}", payjoin_tx.txid().to_string()); Ok(()) } } mod v2 { - use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -381,21 +376,20 @@ mod v2 { use bdk::bitcoin::{Address, Script}; use bdk::wallet::AddressIndex; use http::StatusCode; - use payjoin_ffi::bitcoin::{Network, OutPoint, TxOut}; + use payjoin_ffi::bitcoin::Network; use payjoin_ffi::error::PayjoinError; - use payjoin_ffi::receive::v2::{ - ActiveSession, SessionInitializer, V2PayjoinProposal, V2UncheckedProposal, - }; - use payjoin_ffi::send::v1::RequestBuilder; + use payjoin_ffi::receive::v2::{Receiver, V2PayjoinProposal, V2UncheckedProposal}; + use payjoin_ffi::send::v1::SenderBuilder; use payjoin_ffi::uri::{Uri, Url}; - use payjoin_ffi::OhttpKeys; + use payjoin_ffi::{OhttpKeys, Request}; use reqwest::{Client, ClientBuilder}; use testcontainers::clients::Cli; use testcontainers_modules::redis::Redis; use crate::{ - broadcast_tx, build_original_psbt, extract_pj_tx, init_sender_receiver_wallet, - restore_esplora_client, BoxError, Wallet, + broadcast_tx, build_original_psbt, extract_pj_tx, get_sender_descriptor, + init_sender_receiver_wallet, input_pair_from_local_utxo, restore_rpc_client, BoxError, + Wallet, }; #[tokio::test] @@ -417,8 +411,8 @@ mod v2 { directory: Url, cert_der: Vec, ) -> Result<(), BoxError> { - let (sender, receiver, _) = init_sender_receiver_wallet(); - let esplora_client = restore_esplora_client(); + let (sender, receiver, bitcoind) = init_sender_receiver_wallet(); + let blockchain_client = restore_rpc_client(&bitcoind, &get_sender_descriptor()); let agent = Arc::new(http_agent(cert_der.clone()).unwrap()); wait_for_service_ready(ohttp_relay.clone(), agent.clone()).await?; wait_for_service_ready(directory.clone(), agent.clone()).await?; @@ -427,14 +421,8 @@ mod v2 { .await?; let address = receiver.get_address(AddressIndex::New); // test session with expiry in the future - let session = initialize_session( - address.clone(), - directory.clone(), - ohttp_keys.clone(), - cert_der.clone(), - None, - ) - .await?; + let session = + initialize_session(address.clone(), directory.clone(), ohttp_keys.clone(), None)?; let pj_uri_string = session.pj_uri_builder().amount(5000000).build().as_string(); // Poll receive request let (req, ctx) = session.extract_req()?; @@ -443,13 +431,17 @@ mod v2 { let response_body = session.process_res(response.bytes().await?.to_vec(), ctx).unwrap(); // No proposal yet since sender has not responded assert!(response_body.is_none()); + + // ********************** + // Inside the Sender: + // Create a funded PSBT (not broadcasted) to address with amount given in the pj_uri let pj_uri = Uri::from_str(pj_uri_string).unwrap().check_pj_supported().unwrap(); let psbt = build_original_psbt(&sender, &pj_uri)?; println!("\nOriginal sender psbt: {:#?}", psbt.to_string()); - let req_ctx = RequestBuilder::from_psbt_and_uri(psbt.to_string(), pj_uri)? + let req_ctx = SenderBuilder::from_psbt_and_uri(psbt.to_string(), pj_uri)? .build_recommended(payjoin::bitcoin::FeeRate::BROADCAST_MIN.to_sat_per_kwu())?; - let req_ctx_v2 = req_ctx.extract_v2(Arc::new(directory.to_owned()))?; + let req_ctx_v2 = req_ctx.extract_highest_version(Arc::new(directory.to_owned()))?; let response = agent .post(req_ctx_v2.request.url.as_string()) .header("Content-Type", payjoin::V1_REQ_CONTENT_TYPE) @@ -458,10 +450,12 @@ mod v2 { .await .unwrap(); assert!(response.status().is_success()); - let response_body = - req_ctx_v2.context_v2.process_response(response.bytes().await?.to_vec())?; - // No response body yet since we are async and pushed fallback_psbt to the buffer - assert!(response_body.is_none()); + let send_ctx = req_ctx_v2 + .context + .as_v2() + .unwrap() + .process_response(response.bytes().await?.to_vec())?; + // ********************** // Inside the Receiver: @@ -475,42 +469,41 @@ mod v2 { let response = agent.post(req.url.as_string()).body(req.body).send().await?; let res = response.bytes().await?.to_vec(); payjoin_proposal.process_res(res, ctx)?; - let req_ctx_v2 = req_ctx.extract_v2(Arc::new(directory.to_owned()))?; + + // ********************** + // Inside the Sender: + // Sender checks, signs, finalizes, extracts, and broadcasts + // Replay post fallback to get the response + let (Request { url, body, content_type }, ohttp_ctx) = + send_ctx.extract_req(directory.to_owned().into())?; let response = agent - .post(req_ctx_v2.request.url.as_string()) - .body(req_ctx_v2.request.body) + .post(url.as_string()) + .header("Content-Type", content_type) + .body(body) .send() .await?; let checked_payjoin_proposal_psbt = - req_ctx_v2.context_v2.process_response(response.bytes().await?.to_vec())?.unwrap(); + send_ctx.process_response(response.bytes().await?.to_vec(), &ohttp_ctx)?.unwrap(); let payjoin_tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt.as_str())?; - broadcast_tx(esplora_client, payjoin_tx).unwrap(); + broadcast_tx(blockchain_client, payjoin_tx).unwrap(); Ok(()) } } - async fn initialize_session( + fn initialize_session( address: Address, directory: Url, ohttp_keys: OhttpKeys, - cert_der: Vec, custom_expire_after: Option, - ) -> Result { + ) -> Result { let mock_ohttp_relay = directory.clone(); // pass through to - let initializer = SessionInitializer::new( + Receiver::new( address.to_string(), - custom_expire_after, Network::Regtest, directory, ohttp_keys, mock_ohttp_relay, + custom_expire_after, ) - .unwrap(); - let (req, ctx) = initializer.extract_req()?; - println!("enroll req: {:#?}", &req.url.as_string()); - let response = - http_agent(cert_der).unwrap().post(req.url.as_string()).body(req.body).send().await?; - assert!(response.status().is_success()); - Ok(initializer.process_res(response.bytes().await?.to_vec(), ctx)?) } async fn wait_for_service_ready( service_url: Url, @@ -553,11 +546,8 @@ mod v2 { }) .expect("Receiver should not own any of the inputs"); - // Receive Check 3: receiver can't sign for proposal inputs - let proposal = proposal.check_no_mixed_input_scripts().unwrap(); - - // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. - let payjoin = proposal + // Receive Check 3: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + let wants_outputs = proposal .check_no_inputs_seen_before(|_| Ok(false)) .unwrap() .identify_receiver_outputs(|output_script| { @@ -566,44 +556,26 @@ mod v2 { .map_err(|x| PayjoinError::UnexpectedError { message: x.to_string() }) }) .expect("Receiver should have at least one output"); + _ = wants_outputs.substitute_receiver_script( + receiver.get_address(AddressIndex::New).script_pubkey().into_bytes(), + ); + let wants_inputs = wants_outputs.commit_outputs(); // Select receiver payjoin inputs. TODO Lock them. - let available_inputs = receiver.list_unspent(); - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| { - ( - i.txout.value, - OutPoint { txid: i.outpoint.txid.to_string(), vout: i.outpoint.vout }, - ) - }) - .collect(); - let selected_outpoint = payjoin - .try_preserving_privacy(candidate_inputs) - .expect("receiver input that avoids surveillance not found"); - let selected_utxo = available_inputs - .iter() - .find(|i| { - i.outpoint.txid.to_string() == selected_outpoint.txid - && i.outpoint.vout == selected_outpoint.vout - }) + let available_inputs = receiver + .list_unspent() + .into_iter() + .map(input_pair_from_local_utxo) + .collect::, _>>() .unwrap(); + let selected_outpoint = wants_inputs + .try_preserving_privacy(available_inputs) + .expect("receiver input that avoids surveillance not found"); - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - let txo_to_contribute = TxOut { - value: selected_utxo.txout.value, - script_pubkey: selected_utxo.txout.script_pubkey.clone().into_bytes(), - }; - let outpoint_to_contribute = OutPoint { - txid: selected_utxo.outpoint.txid.to_string(), - vout: selected_utxo.outpoint.vout, - }; - let _ = payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); + let provisional_proposal = + wants_inputs.contribute_inputs(vec![selected_outpoint]).unwrap().commit_inputs(); - _ = payjoin.try_substitute_receiver_output(|| { - Ok(receiver.get_address(AddressIndex::New).script_pubkey().into_bytes()) - }); - let payjoin_proposal = payjoin + let payjoin_proposal = provisional_proposal .finalize_proposal( |psbt| { match receiver.sign( @@ -615,6 +587,7 @@ mod v2 { } }, Some(10), + 100, ) .unwrap(); (*payjoin_proposal).clone() @@ -655,3 +628,26 @@ mod v2 { )) } } + +fn input_pair_from_local_utxo(utxo: LocalUtxo) -> Result { + let psbtin = payjoin::bitcoin::psbt::Input { + // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies + // witness_utxo, even for non-witness inputs + witness_utxo: Some(payjoin::bitcoin::TxOut { + value: payjoin::bitcoin::Amount::from_sat(utxo.txout.value), + script_pubkey: payjoin::bitcoin::Script::from_bytes( + utxo.txout.script_pubkey.as_bytes(), + ) + .into(), + }), + redeem_script: None, // utxo.redeem_script.clone(), + witness_script: None, // utxo.witness_script.clone(), + ..Default::default() + }; + let txin = payjoin::bitcoin::TxIn { + previous_output: payjoin::bitcoin::OutPoint::from_str(&utxo.outpoint.to_string()).unwrap(), + ..Default::default() + }; + InputPair::new(txin.clone().into(), psbtin.clone().into()) + .map_err(|e| format!("Failed to create input pair: {:?}", e).into()) +} diff --git a/tests/bitcoin_core_integration.rs b/tests/bitcoin_core_integration.rs index bde0cca..14e2f5c 100644 --- a/tests/bitcoin_core_integration.rs +++ b/tests/bitcoin_core_integration.rs @@ -4,24 +4,20 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use bitcoincore_rpc::bitcoincore_rpc_json::WalletProcessPsbtResult; -use bitcoincore_rpc::{Auth, Client, RpcApi}; -use payjoin_ffi::bitcoin::{OutPoint, TxOut}; +use bitcoincore_rpc::bitcoincore_rpc_json::{ListUnspentResultEntry, WalletProcessPsbtResult}; +use bitcoincore_rpc::json::AddressType; +use bitcoincore_rpc::{Client, RpcApi}; use payjoin_ffi::receive::v1::{Headers, PayjoinProposal, UncheckedProposal}; -use payjoin_ffi::send::v1::RequestBuilder; +use payjoin_ffi::receive::v2::InputPair; +use payjoin_ffi::send::v1::SenderBuilder; use payjoin_ffi::uri::{PjUriBuilder, Uri, Url}; use payjoin_ffi::Request; type BoxError = Box; -// Set up RPC connections -static RPC_USER: &str = "admin1"; -static RPC_PASSWORD: &str = "123"; -static RPC_HOST: &str = "localhost"; -static RPC_PORT: &str = "18443"; #[test] fn v1_to_v1_full_cycle() -> Result<(), BoxError> { - let (sender, receiver) = init_rpc_sender_receiver(); + let (_bitcoind, sender, receiver) = init_bitcoind_sender_receiver(None, None).unwrap(); // Receiver creates the payjoin URI let pj_receiver_address = receiver.get_new_address(None, None).unwrap().assume_checked(); @@ -34,15 +30,16 @@ fn v1_to_v1_full_cycle() -> Result<(), BoxError> { .amount(832_850) .build() .as_string(); - print!("pj_uri {}", pj_uri_string); + println!("pj_uri {}", pj_uri_string); - let pj_uri = Uri::from_str(pj_uri_string).unwrap(); + // test deseriallize uri + let pj_uri = Uri::from_str(pj_uri_string).unwrap().check_pj_supported().unwrap(); // Sender create a funded PSBT (not broadcasted) to address with amount given in the pj_uri let mut outputs = HashMap::with_capacity(1); outputs.insert( pj_uri.address(), - bitcoincore_rpc::bitcoin::Amount::from_btc(pj_uri.amount().unwrap().clone()).unwrap(), + bitcoincore_rpc::bitcoin::Amount::from_btc(pj_uri.amount().unwrap()).unwrap(), ); let options = bitcoincore_rpc::json::WalletCreateFundedPsbtOptions { @@ -61,26 +58,25 @@ fn v1_to_v1_full_cycle() -> Result<(), BoxError> { .expect("failed to create PSBT") .psbt; let psbt_base64 = sender.wallet_process_psbt(&psbt, None, None, None)?.psbt; - eprintln!("Original psbt: {:#?}", psbt_base64); - let req_ctx = - RequestBuilder::from_psbt_and_uri(psbt_base64, pj_uri.check_pj_supported().unwrap())? - .build_with_additional_fee(10000, None, 0, false)? - .extract_v1()?; + println!("Original psbt: {:#?}", psbt_base64); + let req_ctx = SenderBuilder::from_psbt_and_uri(psbt_base64, pj_uri)? + .build_with_additional_fee(10000, None, 0, false)? + .extract_v1()?; let req = req_ctx.request; - let ctx = req_ctx.context_v1; + let ctx = req_ctx.context; let headers = Headers::from_vec(req.body.clone()); - // ********************** // Inside the Receiver: // this data would transit from one party to another over the network in production let response = handle_pj_request(req, headers, receiver); // this response would be returned as http response to the sender - + println!("process res"); // ********************** // Inside the Sender: // Sender checks, signs, finalizes, extracts, and broadcasts let checked_payjoin_proposal_psbt = - (*ctx).process_response(response?.as_bytes().to_vec()).expect("process error"); + ctx.process_response(response?.as_bytes().to_vec()).expect("process error"); + println!("extract tx"); let tx = extract_pj_tx(&sender, checked_payjoin_proposal_psbt); let txid = broadcast_tx(&sender, tx); println!("Broadcast txid: {:?}", txid); @@ -88,6 +84,7 @@ fn v1_to_v1_full_cycle() -> Result<(), BoxError> { } fn handle_pj_request(req: Request, headers: Headers, receiver: Client) -> Result { + println!("handle req"); let receiver = Arc::new(receiver); let proposal = UncheckedProposal::from_request( req.body.clone(), @@ -131,11 +128,8 @@ fn handle_pj_proposal(proposal: UncheckedProposal, receiver: Arc) -> Arc }) .expect("Receiver should not own any of the inputs"); - // Receive Check 3: receiver can't sign for proposal inputs - let proposal = proposal.check_no_mixed_input_scripts().unwrap(); - - // Receive Check 4: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. - let payjoin = Arc::new( + // Receive Check 3: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + let wants_outputs = Arc::new( proposal .check_no_inputs_seen_before(|_| Ok(false)) .unwrap() @@ -150,35 +144,25 @@ fn handle_pj_proposal(proposal: UncheckedProposal, receiver: Arc) -> Arc }) .expect("Receiver should have at least one output"), ); - - // Select receiver payjoin inputs. TODO Lock them. - let available_inputs = receiver.list_unspent(None, None, None, None, None).unwrap(); - let candidate_inputs: HashMap = available_inputs - .iter() - .map(|i| (i.amount.to_sat(), OutPoint { txid: i.txid.to_string(), vout: i.vout })) - .collect(); - let selected_outpoint = - payjoin.try_preserving_privacy(candidate_inputs).expect("try_preserving_privacy error"); - let selected_utxo = available_inputs - .iter() - .find(|i| { - i.txid.to_string() == selected_outpoint.txid.to_string() - && i.vout == selected_outpoint.vout - }) + let wants_inputs = wants_outputs.commit_outputs(); + + let candidate_inputs = receiver + .list_unspent(None, None, None, None, None) + .expect("Failed to list unspent from bitcoind") + .into_iter() + .map(input_pair_from_list_unspent) + .collect::, _>>() .unwrap(); + let selected_input = wants_inputs + .try_preserving_privacy(candidate_inputs) + .expect("try_preserving_privacy error"); - // calculate receiver payjoin outputs given receiver payjoin inputs and original_psbt, - let txo_to_contribute = TxOut { - value: selected_utxo.amount.to_sat(), - script_pubkey: selected_utxo.script_pub_key.clone().into_bytes(), - }; - let outpoint_to_contribute = - OutPoint { txid: selected_utxo.txid.to_string(), vout: selected_utxo.vout }; - payjoin - .contribute_witness_input(txo_to_contribute, outpoint_to_contribute) - .expect("contribute_witness_input error"); + // txout: txo_to_contribute, + // outpoint: outpoint_to_contribute, + wants_inputs.contribute_inputs(vec![selected_input]).expect("contribute_witness_input error"); + let provisional_proposal = wants_inputs.commit_inputs(); - let payjoin_proposal = payjoin + let payjoin_proposal = provisional_proposal .finalize_proposal( |e| { Ok(receiver @@ -187,22 +171,43 @@ fn handle_pj_proposal(proposal: UncheckedProposal, receiver: Arc) -> Arc .unwrap()) }, Some(1), + 10, ) .expect("Failed to finalize proposal"); payjoin_proposal } -fn init_rpc_sender_receiver() -> (Client, Client) { - let receiver = get_client("receiver"); - let sender = get_client("sender"); - let sender_address = sender.get_new_address(None, None).unwrap().assume_checked(); - let receiver_address = receiver.get_new_address(None, None).unwrap().assume_checked(); - receiver.generate_to_address(11, &receiver_address).unwrap(); - sender.generate_to_address(101, &sender_address).unwrap(); - println!("\n sender balance: {:?}", sender.get_balance(None, None)); - println!("\n receiver balance: {:?}", receiver.get_balance(None, None)); - (sender, receiver) +fn init_bitcoind_sender_receiver( + sender_address_type: Option, + receiver_address_type: Option, +) -> Result<(bitcoind::BitcoinD, bitcoincore_rpc::Client, bitcoincore_rpc::Client), BoxError> { + let bitcoind_exe = std::env::var("BITCOIND_EXE") + .ok() + .or_else(|| bitcoind::downloaded_exe_path().ok()) + .unwrap(); + let conf = bitcoind::Conf::default(); + let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &conf)?; + let receiver = bitcoind.create_wallet("receiver")?; + let receiver_address = receiver.get_new_address(None, receiver_address_type)?.assume_checked(); + let sender = bitcoind.create_wallet("sender")?; + let sender_address = sender.get_new_address(None, sender_address_type)?.assume_checked(); + bitcoind.client.generate_to_address(1, &receiver_address)?; + bitcoind.client.generate_to_address(101, &sender_address)?; + + assert_eq!( + bitcoincore_rpc::bitcoin::Amount::from_btc(50.0)?, + receiver.get_balances()?.mine.trusted, + "receiver doesn't own bitcoin" + ); + + assert_eq!( + bitcoincore_rpc::bitcoin::Amount::from_btc(50.0)?, + sender.get_balances()?.mine.trusted, + "sender doesn't own bitcoin" + ); + Ok((bitcoind, sender, receiver)) } + fn broadcast_tx(client: &Client, tx: payjoin::bitcoin::Transaction) -> Result { let raw_tx_hex = payjoin::bitcoin::consensus::encode::serialize_hex(&tx); Ok(client.send_raw_transaction(raw_tx_hex.as_str())?.to_string()) @@ -221,7 +226,23 @@ fn extract_pj_tx(sender: &Client, psbt: String) -> payjoin::bitcoin::Transaction eprintln!("Sender's Payjoin PSBT: {:#?}", payjoin_psbt); payjoin_psbt.extract_tx().unwrap() } -fn get_client(wallet_name: &str) -> Client { - let url = format!("http://{}:{}/wallet/{}", RPC_HOST, RPC_PORT, wallet_name); - Client::new(&*url, Auth::UserPass(RPC_USER.to_string(), RPC_PASSWORD.to_string())).unwrap() + +fn input_pair_from_list_unspent(utxo: ListUnspentResultEntry) -> Result { + let psbtin = payjoin::bitcoin::psbt::Input { + // NOTE: non_witness_utxo is not necessary because bitcoin-cli always supplies + // witness_utxo, even for non-witness inputs + witness_utxo: Some(payjoin::bitcoin::TxOut { + value: utxo.amount, + script_pubkey: utxo.script_pub_key.clone(), + }), + redeem_script: utxo.redeem_script.clone(), + witness_script: utxo.witness_script.clone(), + ..Default::default() + }; + let txin = payjoin::bitcoin::TxIn { + previous_output: payjoin::bitcoin::OutPoint { txid: utxo.txid, vout: utxo.vout }, + ..Default::default() + }; + InputPair::new(txin.clone().into(), psbtin.clone().into()) + .map_err(|e| format!("Failed to create input pair: {:?}", e).into()) }