From e5d06298be7495afcd422c8da856f147eb82c633 Mon Sep 17 00:00:00 2001 From: DanGould Date: Wed, 16 Oct 2024 12:26:05 -0400 Subject: [PATCH] DRAFT upgrade to payjoin-0.21 --- Cargo.lock | 400 +++++++----------------------- Cargo.toml | 6 +- src/bitcoin.rs | 66 +++++ src/error.rs | 17 +- src/lib.rs | 2 +- src/ohttp.rs | 17 ++ src/receive/v1.rs | 166 ++++++------- src/receive/v2.rs | 354 +++++++++++++------------- 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 | 202 ++++++++------- tests/bitcoin_core_integration.rs | 107 ++++---- 15 files changed, 730 insertions(+), 812 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3226e48..35370bf 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]] @@ -49,17 +49,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "aes" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.9", -] - [[package]] name = "aes-gcm" version = "0.9.2" @@ -67,24 +56,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead 0.4.3", - "aes 0.7.5", + "aes", "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", ] @@ -378,6 +353,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 +393,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" @@ -767,7 +784,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 +807,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" @@ -901,12 +896,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - [[package]] name = "esplora-client" version = "0.6.0" @@ -1087,17 +1076,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 +1102,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" @@ -1225,38 +1179,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "hpke" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf39e5461bfdc6ad0fbc97067519fcaf96a7a2e67f24cc0eb8a1e7c0c45af792" -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", -] - [[package]] name = "http" version = "1.1.0" @@ -1268,17 +1190,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 +1197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -1297,8 +1208,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -1314,30 +1225,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 +1234,8 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1358,22 +1245,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 +1252,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 +1272,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 +1289,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 +1316,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" @@ -1658,29 +1519,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 +1526,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", @@ -1784,16 +1622,14 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "payjoin" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf485245549b366884e295426755ce649d924762f676c1cc00e12e21501884a3" 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 +1640,20 @@ dependencies = [ [[package]] name = "payjoin-directory" version = "0.0.1" -source = "git+https://github.com/payjoin/rust-payjoin#12e08ce3476562e5d22512e17c13281e156bc4b2" 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 +1664,10 @@ version = "0.20.0" dependencies = [ "base64 0.22.1", "bdk", + "bitcoin-ohttp", "bitcoincore-rpc", "hex", - "http 1.1.0", - "ohttp", + "http", "ohttp-relay", "payjoin", "payjoin-directory", @@ -1938,18 +1776,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 +1814,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1998,15 +1824,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 +1941,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 +1955,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", @@ -2213,18 +2033,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 +2040,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" @@ -2633,9 +2432,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" @@ -2794,16 +2593,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 +2741,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http", "httparse", "log", "rand", @@ -3521,17 +3310,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "x25519-dalek" -version = "2.0.0-pre.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "zeroize", -] - [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 6b568da..7282f51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ uniffi = { version = "0.28.0", features = ["bindgen-tests"] } bdk = { version = "0.29.0", features = ["all-keys", "use-esplora-ureq", "keys-bip39"] } bitcoincore-rpc = "0.19.0" http = "1" -payjoin-directory = { git = "https://github.com/payjoin/rust-payjoin", features = ["danger-local-https"] } +payjoin-directory = { path = "../payjoin/payjoin-directory", features = ["danger-local-https"] } ohttp-relay = "0.0.8" rcgen = { version = "0.11" } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } @@ -27,10 +27,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 = {path = "../payjoin/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/src/bitcoin.rs b/src/bitcoin.rs index a8d46c0..2b19bd1 100644 --- a/src/bitcoin.rs +++ b/src/bitcoin.rs @@ -26,6 +26,57 @@ 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 +123,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..52fa73f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,10 @@ 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)] @@ -62,6 +65,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 { @@ -86,6 +98,9 @@ impl_from_error! { ValidationError => ValidationError, CreateRequestError => CreateRequestError, uniffi::UnexpectedUniFFICallbackError => UnexpectedError, + OutputSubstitutionError => OutputSubstitutionError, + InputContributionError => InputContributionError, + PsbtInputError => InputPairError, } 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..49f15a0 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))) @@ -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,53 @@ 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) } } +impl WantsOutputs { + pub fn replace_receiver_outputs( + &self, + 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()) + } -pub trait ProcessPartiallySignedTransaction { - fn callback(&self, psbt: String) -> Result; + pub fn commit_outputs(&self) -> WantsInputs { + self.0.clone().commit_outputs().into() + } } -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( - &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()) +pub struct WantsInputs(payjoin::receive::WantsInputs); + +impl From for WantsInputs { + fn from(value: payjoin::receive::WantsInputs) -> Self { + Self(value) } - #[cfg(feature = "uniffi")] - pub fn try_substitute_receiver_output( +} + +impl WantsInputs { + pub fn contribute_inputs( &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()) + 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 contribute_witness_input( - &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())) + + 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 +324,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 +371,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 +381,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 +391,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 +426,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 +469,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..1f276b3 100644 --- a/src/receive/v2.rs +++ b/src/receive/v2.rs @@ -4,33 +4,18 @@ use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; -use payjoin::bitcoin::psbt::Psbt; -use payjoin::bitcoin::FeeRate; +use payjoin::bitcoin::psbt::{Psbt, Input as PsbtInput}; +use payjoin::bitcoin::{FeeRate, TxIn}; 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 +23,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 +55,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 +75,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 +109,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 +117,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 +129,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 +151,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 +225,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))) @@ -350,7 +273,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 +284,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 +320,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 +346,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 +361,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 +373,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 +470,70 @@ 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: Vec, + ) -> Result { + let candidate_inputs: Vec = candidate_inputs .into_iter() - .map(|(key, value)| (payjoin::bitcoin::Amount::from_sat(key), value.into())) + .map(|pair| pair.into()) .collect(); - match self.mutex_guard().try_preserving_privacy(candidate_inputs) { - Ok(e) => Ok(OutPoint { txid: e.txid.to_string(), vout: e.vout }), + 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_witness_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))) - }) + 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() + } +} + +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 +541,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 +557,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 +567,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 +581,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 +621,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..1029ebc 100644 --- a/tests/bdk_integration_test.rs +++ b/tests/bdk_integration_test.rs @@ -12,13 +12,14 @@ use bitcoincore_rpc::{Auth, Client, RpcApi}; use payjoin_ffi::bitcoin::{OutPoint, TxOut}; 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_USER: &str = "polaruser"; +static RPC_PASSWORD: &str = "polarpass"; static RPC_HOST: &str = "localhost"; static RPC_PORT: &str = "18443"; static ESPLORA_URL: &str = "http://0.0.0.0:30000"; @@ -235,11 +236,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(); - - // 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) + let selected_outpoint = wants_inputs.try_preserving_privacy(available_inputs).expect("gg"); + wants_inputs + .contribute_inputs(vec![selected_outpoint]) .expect("contribute_witness_input error"); + let provisional_proposal = wants_inputs.commit_inputs(); - let payjoin_proposal = payjoin + let payjoin_proposal = provisional_proposal .finalize_proposal( |e| { match receiver @@ -290,6 +273,7 @@ fn handle_proposal(proposal: UncheckedProposal, receiver: Wallet) -> Arc, 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 +540,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 +550,28 @@ 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"); + + let provisional_proposal = wants_inputs + .contribute_witness_inputs(vec![selected_outpoint]) + .unwrap() + .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, - }; - let _ = payjoin.contribute_witness_input(txo_to_contribute, outpoint_to_contribute); - - _ = 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 +583,7 @@ mod v2 { } }, Some(10), + 100, ) .unwrap(); (*payjoin_proposal).clone() @@ -655,3 +624,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..a68a085 100644 --- a/tests/bitcoin_core_integration.rs +++ b/tests/bitcoin_core_integration.rs @@ -4,19 +4,21 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use bitcoincore_rpc::bitcoincore_rpc_json::WalletProcessPsbtResult; +use bitcoincore_rpc::bitcoincore_rpc_json::{ListUnspentResultEntry, WalletProcessPsbtResult}; use bitcoincore_rpc::{Auth, Client, RpcApi}; +use payjoin::bitcoin::TxIn; use payjoin_ffi::bitcoin::{OutPoint, TxOut}; 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_USER: &str = "polaruser"; +static RPC_PASSWORD: &str = "polarpass"; static RPC_HOST: &str = "localhost"; static RPC_PORT: &str = "18443"; #[test] @@ -34,15 +36,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 +64,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 +90,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 +134,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 +150,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,6 +177,7 @@ fn handle_pj_proposal(proposal: UncheckedProposal, receiver: Arc) -> Arc .unwrap()) }, Some(1), + 10, ) .expect("Failed to finalize proposal"); payjoin_proposal @@ -223,5 +214,25 @@ fn extract_pj_tx(sender: &Client, psbt: String) -> payjoin::bitcoin::Transaction } 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() + 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()) }