From 05f1c6c2186fcf76be69d02657be99b7453411c3 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Sat, 10 May 2025 15:24:48 -0300 Subject: [PATCH 01/72] [PDK] Initial approach. --- Cargo.lock | 814 ++++++++++++++++-- liana-gui/Cargo.toml | 3 + liana-gui/src/app/message.rs | 4 +- liana-gui/src/app/state/psbt.rs | 48 ++ liana-gui/src/app/state/receive.rs | 47 +- liana-gui/src/app/state/spend/step.rs | 25 +- liana-gui/src/app/view/message.rs | 4 + liana-gui/src/app/view/psbt.rs | 29 +- liana-gui/src/app/view/receive.rs | 46 + liana-gui/src/app/view/spend/mod.rs | 20 + liana-gui/src/daemon/client/mod.rs | 13 + liana-gui/src/daemon/embedded.rs | 19 + liana-gui/src/daemon/mod.rs | 10 + liana-gui/src/daemon/model.rs | 8 +- .../services/connect/client/backend/mod.rs | 16 + liana/src/descriptors/mod.rs | 11 +- liana/src/signer.rs | 14 +- lianad/Cargo.toml | 5 + lianad/src/bitcoin/poller/looper.rs | 417 ++++++++- lianad/src/commands/mod.rs | 79 +- lianad/src/database/mod.rs | 76 ++ lianad/src/database/sqlite/mod.rs | 151 ++++ lianad/src/database/sqlite/schema.rs | 17 + lianad/src/testutils.rs | 50 +- 24 files changed, 1831 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c38c7907..fc75e374d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", + "rand_core 0.6.4", +] + [[package]] name = "aead" version = "0.5.2" @@ -43,6 +53,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.2.17", + "opaque-debug", +] + [[package]] name = "aes" version = "0.8.4" @@ -50,8 +72,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" +dependencies = [ + "aead 0.4.3", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.7.0", + "ghash 0.4.4", + "subtle", ] [[package]] @@ -60,11 +96,11 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", "subtle", ] @@ -206,7 +242,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand", + "rand 0.8.5", "raw-window-handle", "serde", "serde_repr", @@ -296,7 +332,7 @@ dependencies = [ "ledger-transport-hidapi", "ledger_bitcoin_client", "regex", - "reqwest", + "reqwest 0.11.27", "serde", "serde_bytes", "serde_cbor", @@ -529,6 +565,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bhttp" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef06386f8f092c3419e153a657396e53cafbb901de445a5c54d96ab2ff8c7b2" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "bip329" version = "0.3.0" @@ -619,6 +664,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 0.6.4", + "secp256k1", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -640,6 +704,29 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +[[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 0.9.2", + "bitcoin-hpke", + "byteorder", + "chacha20poly1305 0.8.0", + "hex", + "hkdf 0.11.0", + "lazy_static", + "log", + "rand 0.8.5", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror 1.0.69", + "toml", +] + [[package]] name = "bitcoin-private" version = "0.1.0" @@ -686,6 +773,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin_uri" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0a228e083d1702f83389b0ac71eb70078dc8d7fcbb6cde864d1cbca145f5cc" +dependencies = [ + "bitcoin", + "percent-encoding-rfc3986", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -704,7 +801,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -713,6 +810,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -870,6 +976,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures 0.1.5", + "zeroize", +] + [[package]] name = "chacha20" version = "0.9.1" @@ -877,8 +995,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" +dependencies = [ + "aead 0.4.3", + "chacha20 0.7.1", + "cipher 0.3.0", + "poly1305 0.7.2", + "zeroize", ] [[package]] @@ -887,10 +1018,10 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", + "aead 0.5.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305 0.8.0", "zeroize", ] @@ -908,6 +1039,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.4" @@ -974,13 +1114,13 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aaaf3f7409edc40001c30a4c1337f21558a8ceba2a4afe807da841a38ce83d6" dependencies = [ - "aes", + "aes 0.8.4", "base58", "bitcoin_hashes 0.13.0", - "ctr", + "ctr 0.9.2", "hidapi", "k256", - "rand", + "rand 0.8.5", ] [[package]] @@ -1162,6 +1302,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1218,7 +1367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1230,22 +1379,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "ctor-lite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" +[[package]] +name = "ctr" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a232f92a03f37dd7d7dd2adc67166c77e9cd88de5b019b9a9eecfaeaf7bfd481" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -1261,7 +1430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", @@ -1318,13 +1487,22 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -1470,7 +1648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -1496,7 +1674,7 @@ dependencies = [ "rustls 0.23.22", "serde", "serde_json", - "webpki-roots", + "webpki-roots 0.25.4", "winapi", ] @@ -1508,12 +1686,12 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1689,7 +1867,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1958,8 +2136,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1969,11 +2149,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", "windows-targets 0.52.6", ] +[[package]] +name = "ghash" +version = "0.4.4" +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.1" @@ -1981,7 +2173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", - "polyval", + "polyval 0.6.2", ] [[package]] @@ -2097,7 +2289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -2122,7 +2314,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -2256,13 +2448,42 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac 0.11.0", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2285,6 +2506,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -2292,7 +2524,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -2319,8 +2574,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -2332,6 +2587,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -2339,11 +2613,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.12", + "hyper 0.14.32", "rustls 0.21.12", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.22", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", + "webpki-roots 0.26.11", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -2871,7 +3182,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2", + "sha2 0.10.8", "signature", ] @@ -3027,7 +3338,8 @@ dependencies = [ "lianad", "libc", "log", - "reqwest", + "payjoin", + "reqwest 0.11.27", "rfd", "rust-ini", "serde", @@ -3065,6 +3377,8 @@ dependencies = [ "liana", "log", "miniscript", + "payjoin", + "reqwest 0.11.27", "rusqlite", "serde", "serde_json", @@ -3483,11 +3797,11 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c6159f60beb3bbbcdc266bc789bfc6c37fdad7d7ca7152d3e049ef5af633f0" dependencies = [ - "aes-gcm", + "aes-gcm 0.10.3", "blake2", - "chacha20poly1305", + "chacha20poly1305 0.10.1", "noise-protocol", - "sha2", + "sha2 0.10.8", "x25519-dalek", "zeroize", ] @@ -3963,12 +4277,36 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "payjoin" +version = "0.23.0" +source = "git+https://github.com/0xBEEFCAF3/rust-payjoin.git?rev=619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d#619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d" +dependencies = [ + "bhttp", + "bitcoin", + "bitcoin-hpke", + "bitcoin-ohttp", + "bitcoin_uri", + "http 1.3.1", + "log", + "reqwest 0.12.15", + "serde", + "serde_json", + "url", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "percent-encoding-rfc3986" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3637c05577168127568a64e9dc5a6887da720efef07b3d9472d45f63ab191166" + [[package]] name = "petgraph" version = "0.6.5" @@ -3996,7 +4334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] @@ -4120,15 +4458,38 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.4.0", +] + [[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.4.0", ] [[package]] @@ -4138,9 +4499,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", ] [[package]] @@ -4306,6 +4667,60 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls 0.23.22", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "rand 0.9.1", + "ring", + "rustc-hash 2.1.1", + "rustls 0.23.22", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.38" @@ -4322,8 +4737,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -4333,7 +4758,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -4345,6 +4780,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + [[package]] name = "range-alloc" version = "0.1.4" @@ -4389,7 +4833,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92195228612ac8eed47adbc2ed0f04e513a4ccb98175b6f2bd04d963b533655" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -4487,10 +4931,10 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -4499,14 +4943,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", - "tokio-rustls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", @@ -4514,10 +4958,53 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls 0.27.5", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.22", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.2", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.11", + "windows-registry", +] + [[package]] name = "resvg" version = "0.42.0" @@ -4540,7 +5027,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -4698,11 +5185,23 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -4819,6 +5318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ "bitcoin_hashes 0.14.0", + "rand 0.8.5", "secp256k1-sys", "serde", ] @@ -4937,6 +5437,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.8" @@ -4944,8 +5457,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -4978,8 +5491,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -5253,6 +5766,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -5499,6 +6021,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls 0.23.22", + "tokio", +] + [[package]] name = "tokio-serial" version = "5.4.5" @@ -5552,6 +6084,27 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -5740,6 +6293,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -6099,6 +6662,24 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.8" @@ -6294,6 +6875,41 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -6369,13 +6985,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6394,6 +7026,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6412,6 +7050,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6430,12 +7074,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6454,6 +7110,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6472,6 +7134,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6490,6 +7158,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6508,6 +7182,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winit" version = "0.30.8" @@ -6639,7 +7319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "zeroize", ] diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index bc0768399..391534b64 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -54,6 +54,9 @@ reqwest = { version = "0.11", default-features=false, features = ["json", "rustl rust-ini = "0.19.0" rfd = "0.15.1" +# Payjoin +payjoin = { git = "https://github.com/0xBEEFCAF3/rust-payjoin.git", rev = "619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d", features = ["v2", "io"]} +# payjoin = { path = "../../../rust-payjoin/payjoin", features = ["v2", "io"]} [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index 6abcd912c..cdaed8aeb 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -25,7 +25,7 @@ pub enum Message { DaemonConfigLoaded(Result<(), Error>), LoadWallet(Wallet), Info(Result), - ReceiveAddress(Result<(Address, ChildNumber), Error>), + ReceiveAddress(Result<(Address, ChildNumber, String), Error>), Coins(Result, Error>), /// When we want both coins and tip height together. CoinsTipHeight(Result, Error>, Result), @@ -50,6 +50,8 @@ pub enum Message { BroadcastModal(Result, Error>), RbfModal(Box, bool, Result, Error>), Export(ImportExportMessage), + SendPayjoin(Result<(), Error>), + PayjoinInitiated(Result), } impl From for Message { diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 3734ce06e..f4c73c5e9 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -58,6 +58,7 @@ pub enum PsbtModal { Broadcast(BroadcastModal), Delete(DeleteModal), Export(ExportModal), + SendPayjoin(SendPayjoinModal), } impl<'a> AsRef for PsbtModal { @@ -68,6 +69,7 @@ impl<'a> AsRef for PsbtModal { Self::Broadcast(a) => a, Self::Delete(a) => a, Self::Export(a) => a, + Self::SendPayjoin(a) => a, } } } @@ -80,6 +82,7 @@ impl<'a> AsMut for PsbtModal { Self::Broadcast(a) => a, Self::Delete(a) => a, Self::Export(a) => a, + Self::SendPayjoin(a) => a, } } } @@ -175,6 +178,29 @@ impl PsbtState { Message::View(view::Message::Spend(view::SpendTxMessage::Delete)) => { self.modal = Some(PsbtModal::Delete(DeleteModal::default())); } + Message::View(view::Message::Spend(view::SpendTxMessage::SendPayjoin)) => { + let modal = SendPayjoinModal::new(); + let cmd = modal.load(daemon); + self.modal = Some(PsbtModal::SendPayjoin(modal)); + return cmd; + } + Message::View(view::Message::Spend(view::SpendTxMessage::PayjoinInitiated)) => { + self.tx.status = SpendStatus::PayjoinInitiated; + self.modal = None; + if let Some(bip21) = self.tx.bip21.clone() { + // TODO: remove clone + let psbt = self.tx.psbt.clone(); + return Task::perform( + async move { + daemon + .send_payjoin(bip21.clone(), &psbt) + .await + .map_err(|e| e.into()) + }, + Message::SendPayjoin, + ); + } + } Message::View(view::Message::Spend(view::SpendTxMessage::Sign)) => { if let Some(PsbtModal::Sign(SignModal { display_modal, .. })) = &mut self.modal { *display_modal = true; @@ -280,6 +306,28 @@ impl PsbtState { } } +#[derive(Default)] +pub struct SendPayjoinModal { + _error: Option, +} + +impl SendPayjoinModal { + pub fn new() -> Self { + Self { _error: None } + } +} + +impl Modal for SendPayjoinModal { + fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> { + modal::Modal::new(content, view::psbt::payjoin_send_success_view()) + // On blur, show the psbts view + .on_blur(Some(view::Message::Spend( + view::SpendTxMessage::PayjoinInitiated, + ))) + .into() + } +} + #[derive(Default)] pub struct SaveModal { saved: bool, diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index 9e6c77e74..c05eb26dd 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -7,6 +7,8 @@ use liana::miniscript::bitcoin::{ Address, Network, }; use liana_ui::{component::modal, widget::*}; +use payjoin::io::fetch_ohttp_keys; +use payjoin::{OhttpKeys, Url}; use crate::daemon::model::LabelsLoader; use crate::dir::LianaDirectory; @@ -37,6 +39,7 @@ pub enum Modal { #[derive(Debug, Default)] pub struct Addresses { list: Vec
, + payjoin_uris: HashMap, derivation_indexes: Vec, labels: HashMap, } @@ -53,6 +56,12 @@ impl Labelled for Addresses { } } +#[derive(Clone, Debug)] +struct PayjoinSpecs { + directory: Url, + ohttp_keys: OhttpKeys, +} + pub struct ReceivePanel { data_dir: LianaDirectory, wallet: Arc, @@ -60,10 +69,17 @@ pub struct ReceivePanel { labels_edited: LabelsEdited, modal: Modal, warning: Option, + payjoin_specs: PayjoinSpecs, } impl ReceivePanel { pub fn new(data_dir: LianaDirectory, wallet: Arc) -> Self { + let ohttp_relay = Url::parse("https://pj.bobspacebkk.com").unwrap(); + let directory = Url::parse("https://payjo.in").unwrap(); + let rt = tokio::runtime::Runtime::new().unwrap(); + let ohttp_keys = rt + .block_on(async { fetch_ohttp_keys(ohttp_relay.clone(), directory.clone()).await }) + .unwrap(); Self { data_dir, wallet, @@ -71,6 +87,10 @@ impl ReceivePanel { labels_edited: LabelsEdited::default(), modal: Modal::None, warning: None, + payjoin_specs: PayjoinSpecs { + directory, + ohttp_keys, + }, } } } @@ -83,6 +103,7 @@ impl State for ReceivePanel { self.warning.as_ref(), view::receive::receive( &self.addresses.list, + &self.addresses.payjoin_uris, &self.addresses.labels, self.labels_edited.cache(), ), @@ -129,10 +150,13 @@ impl State for ReceivePanel { } Message::ReceiveAddress(res) => { match res { - Ok((address, derivation_index)) => { + Ok((address, derivation_index, payjoin_uri)) => { self.warning = None; - self.addresses.list.push(address); + self.addresses.list.push(address.clone()); self.addresses.derivation_indexes.push(derivation_index); + self.addresses + .payjoin_uris + .insert(address.to_string(), payjoin_uri); } Err(e) => self.warning = Some(e), } @@ -163,7 +187,7 @@ impl State for ReceivePanel { daemon .get_new_address() .await - .map(|res| (res.address, res.derivation_index)) + .map(|res| (res.address, res.derivation_index, res.payjoin_uri)) .map_err(|e| e.into()) }, Message::ReceiveAddress, @@ -180,6 +204,20 @@ impl State for ReceivePanel { } Task::none() } + Message::View(view::Message::PayjoinInitiate) => { + let daemon = daemon.clone(); + let payjoin_specs = self.payjoin_specs.clone(); + Task::perform( + async move { + daemon + .receive_payjoin(payjoin_specs.directory, payjoin_specs.ohttp_keys) + .await + .map(|res| (res.address, res.derivation_index, res.payjoin_uri)) + .map_err(|e| e.into()) + }, + Message::ReceiveAddress, + ) + } _ => { if let Modal::VerifyAddress(ref mut m) = self.modal { m.update(daemon, cache, message) @@ -352,7 +390,8 @@ mod tests { Some(json!({"method": "getnewaddress", "params": Option::::None})), Ok(json!(GetAddressResult::new( addr.clone(), - ChildNumber::from_normal_idx(0).unwrap() + ChildNumber::from_normal_idx(0).unwrap(), + "".to_string(), ))), )]); let wallet = Arc::new(Wallet::new(LianaDescriptor::from_str(DESC).unwrap())); diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 510a20e82..e2d62cc5d 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -1,7 +1,7 @@ use std::{ cmp::Ordering, collections::{HashMap, HashSet}, - convert::TryInto, + convert::{TryFrom, TryInto}, iter::FromIterator, str::FromStr, sync::Arc, @@ -21,6 +21,7 @@ use liana::{ use lianad::commands::ListCoinsEntry; use liana_ui::{component::form, widget::Element}; +use payjoin::Uri; use crate::{ app::{cache::Cache, error::Error, message::Message, state::psbt, view, wallet::Wallet}, @@ -602,6 +603,20 @@ impl Step for DefineSpend { .update(cache.network, msg); } + view::CreateSpendMessage::Bip21Edited(i, bip21) => { + self.recipients.get_mut(i).unwrap().bip21.value = bip21.clone(); + if let Ok(uri) = Uri::try_from(bip21.as_str()) { + if let Ok(address) = uri.address.require_network(cache.network) { + self.recipients.get_mut(i).unwrap().address.value = + address.to_string(); + } + if let Some(amount) = uri.amount { + self.recipients.get_mut(i).unwrap().amount.value = + amount.to_string_in(Denomination::Bitcoin); + } + } + } + view::CreateSpendMessage::FeerateEdited(s) => { if let Ok(value) = s.parse::() { self.feerate.value = s; @@ -832,6 +847,7 @@ struct Recipient { label: form::Value, address: form::Value, amount: form::Value, + bip21: form::Value, is_recovery: bool, } @@ -910,6 +926,10 @@ impl Recipient { self.label.valid = label.len() <= 100; self.label.value = label; } + view::CreateSpendMessage::Bip21Edited(_, bip21) => { + log::info!("bip21: {}", bip21); + self.bip21.value = bip21; + } _ => {} }; } @@ -922,6 +942,7 @@ impl Recipient { &self.label, is_max_selected, self.is_recovery, + &self.bip21, ) } } @@ -945,6 +966,7 @@ impl SaveSpend { impl Step for SaveSpend { fn load(&mut self, _coins: &[Coin], _tip_height: i32, draft: &TransactionDraft) { let (psbt, warnings) = draft.generated.clone().unwrap(); + let bip21 = draft.recipients.get(0).unwrap().bip21.value.clone(); let mut tx = SpendTx::new( None, psbt, @@ -952,6 +974,7 @@ impl Step for SaveSpend { &self.wallet.main_descriptor, &self.curve, draft.network, + Some(bip21), ); tx.labels.clone_from(&draft.labels); diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 543d80183..df71ac877 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -27,6 +27,7 @@ pub enum Message { HideRescanWarning, ExportPsbt, ImportPsbt, + PayjoinInitiate, } impl Close for Message { @@ -54,6 +55,7 @@ pub enum CreateSpendMessage { Generate, SendMaxToRecipient(usize), Clear, + Bip21Edited(usize, String), } #[derive(Debug, Clone)] @@ -75,6 +77,8 @@ pub enum SpendTxMessage { EditPsbt, PsbtEdited(String), Next, + SendPayjoin, + PayjoinInitiated, } #[derive(Debug, Clone)] diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index ad9413f0d..c41600c54 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -316,6 +316,10 @@ pub fn spend_header<'a>( .into() } +pub fn payjoin_send_success_view<'a>() -> Element<'a, Message> { + card::simple(text("Payjoin sent successfully")).into() +} + pub fn spend_overview_view<'a>( tx: &'a SpendTx, desc_info: &'a LianaPolicy, @@ -394,6 +398,15 @@ pub fn spend_overview_view<'a>( .width(Length::Fixed(150.0)), ) }) + .push_maybe(if tx.path_ready().is_some() { + Some( + button::secondary(None, "Send Payjoin") + .on_press(Message::Spend(SpendTxMessage::SendPayjoin)) + .width(Length::Fixed(150.0)), + ) + } else { + None + }) .align_y(Alignment::Center) .spacing(20), ) @@ -409,7 +422,21 @@ pub fn signatures<'a>( keys_aliases: &'a HashMap, ) -> Element<'a, Message> { Column::new() - .push(if let Some(sigs) = tx.path_ready() { + .push(if tx.status == SpendStatus::PayjoinInitiated { + Container::new(scrollable( + Row::new() + .spacing(5) + .align_y(Alignment::Center) + .spacing(10) + .push(p1_bold("Status")) + .push(icon::circle_check_icon().style(theme::text::warning)) + .push( + text(" Payjoin initiated") + .bold() + .style(theme::text::warning), + ), + )) + } else if let Some(sigs) = tx.path_ready() { Container::new( scrollable( Row::new() diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index fd2cf077c..c0cc3fd37 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -35,6 +35,7 @@ use super::message::Message; pub fn receive<'a>( addresses: &'a [bitcoin::Address], + payjoin_uris: &'a HashMap, labels: &'a HashMap, labels_editing: &'a HashMap>, ) -> Element<'a, Message> { @@ -43,6 +44,11 @@ pub fn receive<'a>( Row::new() .align_y(Alignment::Center) .push(Container::new(h3("Receive")).width(Length::Fill)) + .push( + button::secondary(Some(icon::plus_icon()), "Payjoin") + .on_press(Message::PayjoinInitiate), + ) + .spacing(10) .push( button::primary(Some(icon::plus_icon()), "Generate address") .on_press(Message::Next), @@ -56,6 +62,7 @@ pub fn receive<'a>( Column::new().spacing(10).width(Length::Fill), |col, (i, address)| { let addr = address.to_string(); + let payjoin_uri = payjoin_uris.get(&addr).unwrap(); col.push( card::simple( Column::new() @@ -109,6 +116,45 @@ pub fn receive<'a>( ) .align_y(Alignment::Center), ) + .push(if !payjoin_uri.is_empty() { + Row::new() + .push( + Container::new( + scrollable( + Column::new() + .push(Space::with_height( + Length::Fixed(10.0), + )) + .push( + p2_regular(&payjoin_uri) + .small() + .style(theme::text::secondary), + ) + // Space between the URI and the scrollbar + .push(Space::with_height( + Length::Fixed(10.0), + )), + ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new() + .width(2) + .scroller_width(2), + )), + ) + .width(Length::Fill), + ) + .push( + Button::new( + icon::clipboard_icon() + .style(theme::text::secondary), + ) + .on_press(Message::Clipboard(payjoin_uri.clone())) + .style(theme::button::transparent_border), + ) + .align_y(Alignment::Center) + } else { + Row::new() + }) .push( Row::new() .push( diff --git a/liana-gui/src/app/view/spend/mod.rs b/liana-gui/src/app/view/spend/mod.rs index 06b24ed45..283b0c274 100644 --- a/liana-gui/src/app/view/spend/mod.rs +++ b/liana-gui/src/app/view/spend/mod.rs @@ -354,6 +354,7 @@ pub fn recipient_view<'a>( label: &'a form::Value, is_max_selected: bool, is_recovery: bool, + bip21: &'a form::Value, ) -> Element<'a, CreateSpendMessage> { Container::new( Column::new() @@ -369,6 +370,25 @@ pub fn recipient_view<'a>( ), ), ) + .push( + Row::new() + .align_y(Alignment::Start) + .spacing(10) + .push( + Container::new(p1_bold("BIP21")) + .align_x(alignment::Horizontal::Right) + .padding(10) + .width(Length::Fixed(110.0)), + ) + .push( + form::Form::new_trimmed("BIP21", bip21, move |msg| { + CreateSpendMessage::Bip21Edited(index, msg) + }) + .warning("Invalid BIP21") + .size(P1_SIZE) + .padding(10), + ), + ) .push( Row::new() .align_y(Alignment::Start) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 376aa3fa2..49948f30c 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,6 +5,7 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; +use payjoin::{OhttpKeys, Url}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -87,6 +88,18 @@ impl Daemon for Lianad { self.call("getnewaddress", Option::::None) } + async fn receive_payjoin( + &self, + _directory: Url, + _ohttp_keys: OhttpKeys, + ) -> Result { + unimplemented!() + } + + async fn send_payjoin(&self, _bip21: String, _psbt: &Psbt) -> Result<(), DaemonError> { + unimplemented!() + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index c86483f4d..56eedbb16 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,5 +1,6 @@ use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; +use payjoin::{OhttpKeys, Url}; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; @@ -103,6 +104,24 @@ impl Daemon for EmbeddedDaemon { self.command(|daemon| Ok(daemon.get_new_address())).await } + async fn receive_payjoin( + &self, + directory: Url, + ohttp_keys: OhttpKeys, + ) -> Result { + self.command(|daemon| Ok(daemon.receive_payjoin(directory, ohttp_keys))) + .await + } + + async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError> { + self.command(|daemon| { + daemon + .init_payjoin_sender(bip21, psbt) + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 4afea243d..3a4a41a73 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -20,6 +20,7 @@ use lianad::{ config::Config, StartupError, }; +use payjoin::{OhttpKeys, Url}; use crate::{hw::HardwareWalletConfig, node}; @@ -104,6 +105,12 @@ pub trait Daemon: Debug { async fn stop(&self) -> Result<(), DaemonError>; async fn get_info(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn receive_payjoin( + &self, + directory: Url, + ohttp_keys: OhttpKeys, + ) -> Result; + async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; async fn update_deriv_indexes( &self, receive: Option, @@ -205,6 +212,9 @@ pub trait Daemon: Debug { &info.descriptors.main, &curve, info.network, + // TODO: BIP21 loaded from the backend will always be ignored + // because it is not part of the `ListSpendEntry` + None, )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 9bbb0b94b..befe72869 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -52,6 +52,7 @@ pub struct SpendTx { pub sigs: PartialSpendInfo, pub updated_at: Option, pub kind: TransactionKind, + pub bip21: Option, } #[derive(PartialOrd, Ord, Debug, Clone, PartialEq, Eq)] @@ -60,6 +61,7 @@ pub enum SpendStatus { Broadcast, Spent, Deprecated, + PayjoinInitiated, } impl SpendTx { @@ -70,6 +72,7 @@ impl SpendTx { desc: &LianaDescriptor, secp: &secp256k1::Secp256k1, network: Network, + bip21: Option, ) -> Self { // Use primary path if no inputs are using a relative locktime. let use_primary_path = !psbt @@ -145,7 +148,9 @@ impl SpendTx { // One input coin is missing, the psbt is deprecated for now. if coins_map.len() != psbt.inputs.len() { - status = SpendStatus::Deprecated + // TODO(arturgontijo): Skip for now... + log::warn!("Not deprecating..."); + // status = SpendStatus::Deprecated } let sigs = desc @@ -189,6 +194,7 @@ impl SpendTx { status, sigs, network, + bip21, } } diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index ba58f7132..30c242189 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -16,6 +16,7 @@ use lianad::{ commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, }; +use payjoin::{OhttpKeys, Url}; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; @@ -607,9 +608,22 @@ impl Daemon for BackendWalletClient { Ok(GetAddressResult { address: res.address, derivation_index: res.derivation_index, + payjoin_uri: "bitcoin:".to_string(), }) } + async fn receive_payjoin( + &self, + _directory: Url, + _ohttp_keys: OhttpKeys, + ) -> Result { + unimplemented!() + } + + async fn send_payjoin(&self, _bip21: String, _psbt: &Psbt) -> Result<(), DaemonError> { + unimplemented!() + } + async fn update_deriv_indexes( &self, _receive: Option, @@ -1233,6 +1247,8 @@ fn spend_tx_from_api( desc, secp, network, + // TODO: BIP21 loaded from the backend will always be ignored + None, ); tx.load_labels(&labels); tx diff --git a/liana/src/descriptors/mod.rs b/liana/src/descriptors/mod.rs index f89577f7f..900969f0a 100644 --- a/liana/src/descriptors/mod.rs +++ b/liana/src/descriptors/mod.rs @@ -1,3 +1,4 @@ +use log::warn; use miniscript::{ bitcoin::{ self, @@ -445,7 +446,15 @@ impl LianaDescriptor { if txin.sequence != first_txin.sequence || spend_info != self.partial_spend_info_txin(psbt_in, txin) { - return Err(LianaDescError::InconsistentPsbt); + // TODO(arturgontijo): Skip for now + warn!( + "LianaDescError::InconsistentPsbt -> {} != {} || {:?} != {:?}", + txin.sequence, + first_txin.sequence, + spend_info, + self.partial_spend_info_txin(psbt_in, txin) + ); + // return Err(LianaDescError::InconsistentPsbt); } } diff --git a/liana/src/signer.rs b/liana/src/signer.rs index bbf1b2ce5..8f5f940d7 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -367,20 +367,24 @@ impl HotSigner { .iter() .filter_map(|psbt_in| psbt_in.witness_utxo.clone()) .collect(); - if prevouts.len() != psbt.inputs.len() { - return Err(SignerError::IncompletePsbt); - } + // TODO(arturgontijo): Skip for now... + // if prevouts.len() != psbt.inputs.len() { + // return Err(SignerError::IncompletePsbt); + // } // Sign each input in the PSBT. for i in 0..psbt.inputs.len() { if psbt.inputs[i].witness_script.is_some() { - self.sign_p2wsh( + match self.sign_p2wsh( secp, &mut sighash_cache, master_fingerprint, &mut psbt.inputs[i], i, - )?; + ) { + Ok(_) => log::info!("Signed input at {}", i), + Err(err) => log::warn!("Didnt sign input at {} | {}", i, err), + } } else { self.sign_taproot( secp, diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index e9e5eea62..c81dfc233 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -58,3 +58,8 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal # import/export labels bip329 = { version = "0.3.0", default-features = false } + +# Payjoin +payjoin = { git = "https://github.com/0xBEEFCAF3/rust-payjoin.git", rev = "619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d", features = ["v2", "io"]} +# payjoin = { path = "../../../rust-payjoin/payjoin", features = ["v2", "io"]} +reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index 74c9ba8e3..07bdd2663 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -1,12 +1,37 @@ use crate::{ bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress}, - database::{Coin, DatabaseConnection, DatabaseInterface}, + database::{ + sqlite::{PayjoinReceiverStatus, PayjoinSenderStatus}, + Coin, CoinStatus, DatabaseConnection, DatabaseInterface, + }, }; -use std::{collections::HashSet, convert::TryInto, sync, thread, time}; +use std::{ + collections::HashSet, + convert::{TryFrom, TryInto}, + error::Error, + str::FromStr, + sync, thread, time, +}; use liana::descriptors; -use miniscript::bitcoin::{self, secp256k1}; +// use log::{error, info, warn}; +use miniscript::{ + bitcoin::{self, secp256k1}, + psbt::PsbtExt, +}; +use payjoin::{ + bitcoin::{ + psbt::{Input, Output}, + Amount, Psbt, ScriptBuf, Sequence, TxIn, TxOut, + }, + persist::NoopPersister, + receive::v2::{UncheckedProposal, WantsOutputs}, + send::v2::{Sender, SenderBuilder}, + Uri, UriExt, Url, +}; + +const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; #[derive(Debug, Clone)] struct UpdatedCoins { @@ -395,6 +420,390 @@ pub fn sync_poll_interval() -> time::Duration { time::Duration::from_secs(0) } +fn http_agent() -> reqwest::blocking::Client { + reqwest::blocking::Client::new() +} + +fn post_request(req: payjoin::Request) -> Result> { + let http = http_agent(); + match http + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + { + Ok(r) => Ok(r), + Err(err) => Err(format!("Failed to post_reques(): {}", err).into()), + } +} + +fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1) { + let mut witness_utxo_to_clean = vec![]; + let mut inputs_to_finalize = vec![]; + for (index, input) in psbt.inputs.iter_mut().enumerate() { + if input.witness_utxo.is_none() { + // finalize_proposal() cleans this up, but we need it to finalize_inp_mut() bellow + input.witness_utxo = Some(TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::default(), + }); + witness_utxo_to_clean.push(index); + continue; + } + if input.final_script_sig.is_some() + || input.final_script_witness.is_some() + || input.partial_sigs.is_empty() + { + continue; + } + inputs_to_finalize.push(index); + } + + for index in inputs_to_finalize { + log::info!("Finalizing input at: {}", index); + psbt.finalize_inp_mut(&secp, index).unwrap(); + } + + for index in witness_utxo_to_clean { + psbt.inputs[index].witness_utxo = None; + } +} + +fn proposal_dummy_checks_bypass(proposal: &UncheckedProposal) -> WantsOutputs { + let proposal = proposal + .clone() + .check_broadcast_suitability(None, |_| Ok(true)) + .expect("Failed to check broadcast suitability"); + let proposal = proposal + .check_inputs_not_owned(|_input| Ok(false)) + .expect("Failed to check inputs not owned"); + let proposal = proposal + .check_no_inputs_seen_before(|_| Ok(false)) + .expect("Failed to check no inputs seen before"); + let proposal = proposal + .identify_receiver_outputs(|_| Ok(true)) + .expect("Failed to identify receiver outputs"); + proposal +} + +fn process_proposal_psbt( + proposal: &UncheckedProposal, + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result { + let desc = descs.first().unwrap(); + + let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); + if let Some((_, coin)) = coins.iter().next() { + let txs = db_conn.list_wallet_transactions(&[coin.outpoint.txid]); + let (db_tx, _, _) = txs.first().unwrap(); + + let tx = db_tx.clone(); + let txin = TxIn { + previous_output: coin.outpoint, + // TODO(arturgontijo): Avoiding Validation(ValidationError(Proposal(MixedSequence))) + sequence: Sequence::from_hex("0xFFFFFFFD").unwrap(), + ..Default::default() + }; + + let txout = tx.tx_out(coin.outpoint.vout as usize).unwrap().clone(); + + let derived = desc.derive(coin.derivation_index, secp); + + let mut psbtin = Input { + non_witness_utxo: Some(tx.clone()), + witness_utxo: Some(txout.clone()), + ..Default::default() + }; + derived.update_psbt_in(&mut psbtin); + + let proposal = proposal_dummy_checks_bypass(&proposal); + let proposal = proposal.commit_outputs(); + let proposa = proposal.commit_inputs(); + + // let inputs = vec![InputPair::new(txin.clone(), psbtin.clone()).unwrap()]; + // let provisional_proposal = proposal.contribute_inputs(inputs).unwrap().commit_inputs(); + + let proposal = proposa + .finalize_proposal( + |psbt| Ok(psbt.clone()), + None, + Some(bitcoin::FeeRate::from_sat_per_vb(150).unwrap()), + ) + .expect("Failed to finalize proposal"); + + let mut psbt = proposal.psbt().clone(); + + psbt.inputs.push(psbtin); + psbt.unsigned_tx.input.push(txin); + + let output = TxOut { + value: coin.amount, + script_pubkey: derived.script_pubkey(), + }; + psbt.outputs.push(Output::default()); + psbt.unsigned_tx.output.push(output); + + return Ok(psbt); + } + + Err(()) +} + +pub fn payjoin_receiver_check( + db_conn: &mut Box, + _bitcoind: &mut impl BitcoinInterface, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) { + let mut payjoin_receivers = db_conn.get_all_payjoin_receivers(); + for (address, status, receiver, psbt) in payjoin_receivers.iter_mut() { + log::info!("[Payjoin] PayjoinReceiverStatus: {:?}", status); + match status { + PayjoinReceiverStatus::Pending => { + log::info!("[Payjoin] receiver: {}", receiver.pj_uri()); + let (req, ctx) = receiver + .extract_req(OHTTP_RELAY) + .expect("Failed to extract request"); + match post_request(req.clone()) { + Ok(resp) => { + let proposal = receiver + .process_res( + resp.bytes().expect("Failed to read response").as_ref(), + ctx, + ) + .expect("Failed to process response"); + if let Some(proposal) = proposal { + log::info!("[Payjoin] receiver got a proposal..."); + let new_psbt = + process_proposal_psbt(&proposal, db_conn, descs, secp).unwrap(); + + // DB logic + log::info!("[Payjoin] Inserting PSBT and STATUS..."); + db_conn.update_payjoin_receiver_status( + &address, + PayjoinReceiverStatus::Signing, + new_psbt.to_string(), + ); + db_conn.store_spend(&new_psbt); + + log::info!("[Payjoin] PSBT in DB: {}", new_psbt.to_string()); + } else { + log::info!("empty proposal") + } + } + Err(err) => { + log::error!("[Payjoin] payjoin_receiver_check(): {} -> {}", req.url, err) + } + } + } + PayjoinReceiverStatus::Signing => { + let psbt = match Psbt::from_str(psbt) { + Ok(psbt) => psbt, + Err(err) => { + log::error!( + "[Payjoin] payjoin_receiver_check(psbt_from): len={} -> {}", + psbt.len(), + err + ); + continue; + } + }; + let txid = psbt.unsigned_tx.compute_txid(); + match db_conn.spend_tx(&txid) { + Some(psbt) => { + let mut is_signed = false; + for psbtin in &psbt.inputs { + if !psbtin.partial_sigs.is_empty() { + log::info!("PSBT was signed!"); + is_signed = true; + break; + } + } + + if is_signed { + let mut psbt = psbt.clone(); + + let (req, ctx) = receiver + .extract_req( + Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"), + ) + .expect("Failed to extract request"); + match post_request(req.clone()) { + Ok(resp) => { + let proposal = receiver + .process_res( + resp.bytes().expect("Failed to read response").as_ref(), + ctx, + ) + .expect("Failed to process response"); + if let Some(proposal) = proposal { + log::info!("[Payjoin] receiver got a proposal..."); + + finalize_psbt(&mut psbt, secp); + + let proposal = proposal_dummy_checks_bypass(&proposal); + let proposal = proposal.commit_outputs(); + let proposal = proposal.commit_inputs(); + + let mut proposal = proposal + .finalize_proposal( + |_| Ok(psbt.clone()), + None, + Some( + bitcoin::FeeRate::from_sat_per_vb(150).unwrap(), + ), + ) + .expect("Failed to finalize proposal"); + + let (req, ctx) = proposal + .extract_req( + Url::from_str(OHTTP_RELAY) + .expect("Invalid OHTTP relay"), + ) + .expect("Failed to extract request"); + + // Respond to sender + log::info!("[Payjoin] receiver responding to sender..."); + match post_request(req.clone()) { + Ok(resp) => { + let _proposal = receiver + .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) + .expect("Failed to process response"); + // Update status of receiver + db_conn.update_payjoin_receiver_status( + &address, + PayjoinReceiverStatus::Completed, + psbt.to_string(), + ); + }, + Err(err) => log::error!( + "[Payjoin] payjoin_receiver_check(respond_to_sender): {} -> {}", + req.url, err + ), + } + } else { + log::info!("empty proposal") + } + } + Err(err) => log::error!( + "[Payjoin] payjoin_receiver_check(Signed): {} -> {}", + req.url, + err + ), + } + } + } + None => {} + } + } + _ => {} + } + } +} + +fn payjoin_sender_check( + db_conn: &mut Box, + secp: &secp256k1::Secp256k1, +) { + let payjoin_senders = db_conn.get_all_payjoin_senders(); + for (bip21, txid, _) in payjoin_senders { + log::info!("Payjoin sender: {}", bip21); + let mut psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); + let pj_uri = Uri::try_from(bip21.as_str()) + .expect("Invalid BIP21") + .assume_checked() + .check_pj_supported() + .expect("Invalid PJ BIP21"); + + // Clone the psbt to finalize, and copy over the witness, + // this is bc finalize mut will remove those fields and we need them again when we sign the pj psbt + let mut psbt_to_finalize = psbt.clone(); + psbt_to_finalize + .finalize_mut(secp) + .expect("finalize should work"); + + for (i, input) in psbt_to_finalize.inputs.iter_mut().enumerate() { + psbt.inputs[i].final_script_sig = input.final_script_sig.clone(); + psbt.inputs[i].final_script_witness = input.final_script_witness.clone(); + } + + let new_sender = SenderBuilder::new(psbt, pj_uri) + .build_recommended(bitcoin::FeeRate::BROADCAST_MIN) + .expect("Failed to build sender"); + + // TODO: should just be able to load a sender from the db, and not use the NoopPersister. + let sender_storage_token = new_sender + .persist(&mut NoopPersister) + .expect("Failed to persist sender"); + + let sender = + Sender::load(sender_storage_token, &NoopPersister).expect("Failed to load sender"); + + let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); + let (post_req, post_ctx) = sender.extract_v2(ohttp_url).expect("Failed to extract v2"); + // Send original PSBT to the receiver via the BIP77 directory + match post_request(post_req.clone()) { + Ok(resp) => { + let get_ctx = post_ctx + .process_response(resp.bytes().expect("Failed to read response").as_ref()) + .expect("Failed to process response"); + + // Read the response from the receiver via the BIP77 directory + let (get_req, ohttp_ctx) = get_ctx + .extract_req(OHTTP_RELAY) + .expect("Failed to extract get request"); + + match post_request(get_req.clone()) { + Ok(resp) => { + log::info!("Payjoin sender got a response..."); + + let psbt = match get_ctx.process_response( + resp.bytes().expect("Failed to read response").as_ref(), + ohttp_ctx, + ) { + Ok(Some(psbt)) => psbt, + Ok(None) => { + // nothing to do yet, no response + log::warn!("Nothing to do yet, no response..."); + continue; + } + Err(e) => { + log::warn!("Failed to process payjoin sender response: {:?}", e); + // TODO: handle error + continue; + } + }; + // Store updated Payjoin psbt + log::info!( + "Updated Payjoin psbt: {} -> {}", + txid, + psbt.unsigned_tx.compute_txid() + ); + db_conn.store_spend(&psbt); + log::info!("Deleting Payjoin psbt (txid={})", txid); + db_conn.delete_spend(&txid); + + // Mark the sender as completed + db_conn.update_payjoin_sender_status(txid, PayjoinSenderStatus::Completed); + } + Err(err) => log::error!( + "payjoin_sender_check(getting_psbt): {} -> {}", + get_req.url, + err + ), + } + } + Err(err) => log::error!( + "payjoin_sender_check(sending_og_psbt): {} -> {}", + post_req.url, + err + ), + } + } +} + /// Update our state from the Bitcoin backend. pub fn poll( bit: &mut sync::Arc>, @@ -405,6 +814,8 @@ pub fn poll( let mut db_conn = db.connection(); updates(&mut db_conn, bit, descs, secp); rescan_check(&mut db_conn, bit, descs, secp); + payjoin_sender_check(&mut db_conn, secp); + payjoin_receiver_check(&mut db_conn, bit, descs, secp); let now: u32 = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .expect("current system time must be later than epoch") diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 7162b63a3..7a34824cb 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -43,6 +43,7 @@ use miniscript::{ }, psbt::PsbtExt, }; +use payjoin::{persist::NoopPersister, OhttpKeys, Url}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -359,7 +360,49 @@ impl DaemonControl { .receive_descriptor() .derive(new_index, &self.secp) .address(self.config.bitcoin_config.network); - GetAddressResult::new(address, new_index) + GetAddressResult::new(address, new_index, "".to_string()) + } + + pub fn receive_payjoin(&self, directory: Url, ohttp_keys: OhttpKeys) -> GetAddressResult { + let mut db_conn = self.db.connection(); + let index = db_conn.receive_index(); + let new_index = index + .increment() + .expect("Can't get into hardened territory"); + db_conn.set_receive_index(new_index, &self.secp); + let address = self + .config + .main_descriptor + .receive_descriptor() + .derive(new_index, &self.secp) + .address(self.config.bitcoin_config.network); + + let pj_receiver = payjoin::receive::v2::NewReceiver::new( + address.clone(), + directory.clone(), + ohttp_keys.clone(), + None, + ) + .unwrap(); + + let storage_token = pj_receiver.persist(&mut NoopPersister).unwrap(); + let receiver = + payjoin::receive::v2::Receiver::load(storage_token, &mut NoopPersister).unwrap(); + + let mut payjoin_uri = receiver.pj_uri(); + // HACK: hardcoded amount for now + payjoin_uri.amount = Some(bitcoin::Amount::from_sat(10_000)); + + db_conn.create_payjoin_receiver(&address, receiver.clone(), "".to_string()); + GetAddressResult::new(address, new_index, payjoin_uri.to_string()) + } + + /// Initate a payjoin sender + pub fn init_payjoin_sender(&self, bip21: String, psbt: &Psbt) -> Result<(), CommandError> { + let mut db_conn = self.db.connection(); + let txid = psbt.clone().extract_tx().unwrap().compute_txid(); + db_conn.create_payjoin_sender(bip21, txid); + Ok(()) } /// Update derivation indexes @@ -807,14 +850,24 @@ impl DaemonControl { let mut spend_psbt = db_conn .spend_tx(txid) .ok_or(CommandError::UnknownSpend(*txid))?; - spend_psbt.finalize_mut(&self.secp).map_err(|e| { - CommandError::SpendFinalization( - e.into_iter() - .next() - .map(|e| e.to_string()) - .unwrap_or_default(), - ) - })?; + + let mut inputs_to_finalize = vec![]; + for (index, input) in spend_psbt.inputs.iter().enumerate() { + if input.final_script_sig.is_some() + || input.final_script_witness.is_some() + || input.partial_sigs.is_empty() + { + continue; + } + inputs_to_finalize.push(index); + } + + for index in inputs_to_finalize { + log::info!("Finalizing input at: {}", index); + spend_psbt + .finalize_inp_mut(&self.secp, index) + .map_err(|e| CommandError::SpendFinalization(e.to_string()))?; + } // Then, broadcast it (or try to, we never know if we are not going to hit an // error at broadcast time). @@ -1278,13 +1331,19 @@ pub struct GetAddressResult { #[serde(deserialize_with = "deser_addr_assume_checked")] pub address: bitcoin::Address, pub derivation_index: bip32::ChildNumber, + pub payjoin_uri: String, } impl GetAddressResult { - pub fn new(address: bitcoin::Address, derivation_index: bip32::ChildNumber) -> Self { + pub fn new( + address: bitcoin::Address, + derivation_index: bip32::ChildNumber, + payjoin_uri: String, + ) -> Self { Self { address, derivation_index, + payjoin_uri, } } } diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 57c4a835a..fde66d5b5 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -22,6 +22,8 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; +use payjoin::receive::v2::Receiver; +use sqlite::{PayjoinReceiverStatus, PayjoinSenderStatus}; /// Information about the wallet. /// @@ -194,6 +196,40 @@ pub trait DatabaseConnection { /// Dump all labels fn get_labels_bip329(&mut self, offset: u32, limit: u32) -> Labels; + + /// Payjoin + + /// Create a payjoin receiver + fn create_payjoin_receiver( + &mut self, + address: &bitcoin::Address, + receiver: Receiver, + psbt_str: String, + ); + /// Get a all active payjoin receivers + fn get_all_payjoin_receivers( + &mut self, + ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)>; + /// Update the status of a payjoin receiver + fn update_payjoin_receiver_status( + &mut self, + address: &bitcoin::Address, + status: PayjoinReceiverStatus, + psbt_str: String, + ); + + /// Create a payjoin sender + fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid); + /// Get a all active payjoin senders + fn get_all_payjoin_senders(&mut self) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus)>; + /// Update the status of a payjoin sender + fn update_payjoin_sender_status( + &mut self, + spend_tx_id: bitcoin::Txid, + status: PayjoinSenderStatus, + ); + + // ------- } impl DatabaseConnection for SqliteConn { @@ -416,6 +452,46 @@ impl DatabaseConnection for SqliteConn { }) .collect() } + + fn create_payjoin_receiver( + &mut self, + address: &bitcoin::Address, + receiver: Receiver, + psbt_str: String, + ) { + self.create_payjoin_receiver(address, receiver, psbt_str) + } + + fn get_all_payjoin_receivers( + &mut self, + ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)> { + self.get_all_payjoin_receivers() + } + + fn update_payjoin_receiver_status( + &mut self, + address: &bitcoin::Address, + status: PayjoinReceiverStatus, + psbt_str: String, + ) { + self.update_payjoin_receiver_status(address, status, psbt_str) + } + + fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid) { + self.create_payjoin_sender(bip21, spend_tx_id) + } + + fn get_all_payjoin_senders(&mut self) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus)> { + self.get_all_payjoin_senders() + } + + fn update_payjoin_sender_status( + &mut self, + spend_tx_id: bitcoin::Txid, + status: PayjoinSenderStatus, + ) { + self.update_payjoin_sender_status(spend_tx_id, status) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 5f6876ee0..8604fd3ce 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -27,12 +27,14 @@ use crate::{ }, }; use liana::descriptors::LianaDescriptor; +use payjoin::receive::v2::Receiver; use std::{ cmp, collections::{HashMap, HashSet}, convert::TryInto, fmt, io, path, + str::FromStr, }; use miniscript::bitcoin::{ @@ -213,6 +215,48 @@ impl SqliteDb { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PayjoinSenderStatus { + Pending = 0, + Signing = 1, + Completed = 2, + // TODO: more specific enums for why it failed + Failed = 3, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PayjoinReceiverStatus { + Pending = 0, + Signing = 1, + Completed = 2, + // TODO: more specific enums for why it failed + Failed = 3, +} + +impl From for PayjoinSenderStatus { + fn from(status: i32) -> Self { + match status { + 0 => PayjoinSenderStatus::Pending, + 1 => PayjoinSenderStatus::Signing, + 2 => PayjoinSenderStatus::Completed, + 3 => PayjoinSenderStatus::Failed, + _ => panic!("Invalid payjoin sender status: {}", status), + } + } +} + +impl From for PayjoinReceiverStatus { + fn from(status: i32) -> Self { + match status { + 0 => PayjoinReceiverStatus::Pending, + 1 => PayjoinReceiverStatus::Signing, + 2 => PayjoinReceiverStatus::Completed, + 3 => PayjoinReceiverStatus::Failed, + _ => panic!("Invalid payjoin receiver status: {}", status), + } + } +} + // We only support single wallet. The id of the wallet row is always 1. const WALLET_ID: i64 = 1; @@ -963,6 +1007,113 @@ impl SqliteConn { }) .expect("Db must not fail"); } + + /// Create a payjoin receiver, TODO: strong type to bitcoin::Address? + pub fn create_payjoin_receiver( + &mut self, + address: &bitcoin::Address, + receiver: Receiver, + psbt_str: String, + ) { + let receiver_json = serde_json::to_string(&receiver).unwrap(); + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "INSERT INTO payjoin_receivers (address, status, receiver, psbt) VALUES (?1, ?2, ?3, ?4)", + rusqlite::params![ + address.to_string(), + PayjoinReceiverStatus::Pending as i32, + receiver_json, + psbt_str + ], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + pub fn update_payjoin_receiver_status( + &mut self, + address: &bitcoin::Address, + status: PayjoinReceiverStatus, + psbt_str: String, + ) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_receivers SET status = ?1, psbt = ?2 WHERE address = ?3", + rusqlite::params![status as i32, psbt_str, address.to_string()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + pub fn get_all_payjoin_receivers( + &mut self, + ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)> { + db_query( + &mut self.conn, + "SELECT address, status, receiver, psbt FROM payjoin_receivers", + rusqlite::params![], + |row| { + let address_str: String = row.get(0)?; + let address = bitcoin::Address::from_str(&address_str) + .unwrap() + .assume_checked(); + let status: i32 = row.get(1)?; + let receiver_json: String = row.get(2)?; + let receiver: Receiver = serde_json::from_str(&receiver_json).unwrap(); + let psbt_str: String = row.get(3)?; + Ok((address, status.into(), receiver, psbt_str)) + }, + ) + .expect("Db must not fail") + } + + /// Create a payjoin sender + pub fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid) { + let status = PayjoinSenderStatus::Pending; + let txid = spend_tx_id[..].to_vec(); + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "INSERT INTO payjoin_senders (bip21, spend_tx_id, status) VALUES (?1, ?2, ?3)", + rusqlite::params![bip21, txid, status as i32], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + pub fn get_all_payjoin_senders(&mut self) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus)> { + db_query( + &mut self.conn, + "SELECT bip21, spend_tx_id, status FROM payjoin_senders WHERE status = ?1", + rusqlite::params![PayjoinSenderStatus::Pending as i32], + |row| { + let bip21: String = row.get(0)?; + let spend_tx_id: Vec = row.get(1)?; + let txid: bitcoin::Txid = + encode::deserialize(&spend_tx_id).expect("We only store valid txids"); + let status: i32 = row.get(2)?; + Ok((bip21, txid, PayjoinSenderStatus::from(status))) + }, + ) + .expect("Db must not fail") + } + + pub fn update_payjoin_sender_status( + &mut self, + spend_tx_id: bitcoin::Txid, + status: PayjoinSenderStatus, + ) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_senders SET status = ?1 WHERE spend_tx_id = ?2", + rusqlite::params![status as i32, spend_tx_id[..].to_vec()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } } #[cfg(test)] diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 34eb5fd29..c4cad306f 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -122,6 +122,23 @@ CREATE TABLE labels ( item TEXT UNIQUE NOT NULL, value TEXT NOT NULL ); + +/* Payjoin senders */ +CREATE TABLE payjoin_senders ( + id INTEGER PRIMARY KEY NOT NULL, + bip21 TEXT NOT NULL, + spend_tx_id BLOB UNIQUE NOT NULL, + status INTEGER NOT NULL CHECK (status IN (0,1,2,3,4)) +); + +/* Payjoin receivers */ +CREATE TABLE payjoin_receivers ( + id INTEGER PRIMARY KEY NOT NULL, + address TEXT UNIQUE NOT NULL, + status INTEGER NOT NULL CHECK (status IN (0,1,2,3,4)), + receiver TEXT NOT NULL, + psbt TEXT NOT NULL +); "; /// A row in the "tip" table. diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 2f4a11bae..d4abf93c4 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -2,12 +2,14 @@ use crate::{ bitcoin::{BitcoinInterface, Block, BlockChainTip, MempoolEntry, SyncProgress, UTxO}, config::{BitcoinConfig, Config}, database::{ - BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet, + sqlite::PayjoinReceiverStatus, BlockInfo, Coin, CoinStatus, DatabaseConnection, + DatabaseInterface, LabelItem, Wallet, }, datadir::DataDirectory, DaemonControl, DaemonHandle, }; use liana::descriptors; +use payjoin::receive::v2::Receiver; use std::convert::TryInto; use std::{ @@ -528,6 +530,52 @@ impl DatabaseConnection for DummyDatabase { fn get_labels_bip329(&mut self, _offset: u32, _limit: u32) -> bip329::Labels { todo!() } + + fn create_payjoin_receiver( + &mut self, + _address: &bitcoin::Address, + _receiver: Receiver, + _psbt: String, + ) { + todo!() + } + + fn get_all_payjoin_receivers( + &mut self, + ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)> { + todo!() + } + + fn update_payjoin_receiver_status( + &mut self, + _address: &bitcoin::Address, + _status: PayjoinReceiverStatus, + _psbt_str: String, + ) { + todo!() + } + + fn create_payjoin_sender(&mut self, _bip21: String, _spend_tx_id: bitcoin::Txid) { + todo!() + } + + fn get_all_payjoin_senders( + &mut self, + ) -> Vec<( + String, + bitcoin::Txid, + crate::database::sqlite::PayjoinSenderStatus, + )> { + todo!() + } + + fn update_payjoin_sender_status( + &mut self, + _spend_tx_id: bitcoin::Txid, + _status: crate::database::sqlite::PayjoinSenderStatus, + ) { + todo!() + } } pub struct DummyLiana { From 2e4c0354d451b7699939c12ca20ba046f0763f74 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Mon, 12 May 2025 13:00:14 -0300 Subject: [PATCH 02/72] [PDK] Use rust-payjoin branch that accepts AddressType::P2wsh --- Cargo.lock | 2 +- liana-gui/Cargo.toml | 3 +- liana/src/descriptors/mod.rs | 8 +-- lianad/Cargo.toml | 3 +- lianad/src/bitcoin/poller/looper.rs | 76 ++++++++++++++++------------- lianad/src/commands/mod.rs | 19 ++------ 6 files changed, 49 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc75e374d..0b7e8905e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.23.0" -source = "git+https://github.com/0xBEEFCAF3/rust-payjoin.git?rev=619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d#619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d" +source = "git+https://github.com/arturgontijo/rust-payjoin.git?branch=liana-poc#05ca2e8e4606315321634d4af64207253ecb0d1c" dependencies = [ "bhttp", "bitcoin", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 391534b64..159a2462d 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,8 +55,7 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -payjoin = { git = "https://github.com/0xBEEFCAF3/rust-payjoin.git", rev = "619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d", features = ["v2", "io"]} -# payjoin = { path = "../../../rust-payjoin/payjoin", features = ["v2", "io"]} +payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/liana/src/descriptors/mod.rs b/liana/src/descriptors/mod.rs index 900969f0a..fd55488af 100644 --- a/liana/src/descriptors/mod.rs +++ b/liana/src/descriptors/mod.rs @@ -447,13 +447,7 @@ impl LianaDescriptor { || spend_info != self.partial_spend_info_txin(psbt_in, txin) { // TODO(arturgontijo): Skip for now - warn!( - "LianaDescError::InconsistentPsbt -> {} != {} || {:?} != {:?}", - txin.sequence, - first_txin.sequence, - spend_info, - self.partial_spend_info_txin(psbt_in, txin) - ); + warn!("LianaDescError::InconsistentPsbt: Not throwing..."); // return Err(LianaDescError::InconsistentPsbt); } } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index c81dfc233..188fdda29 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,6 +60,5 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -payjoin = { git = "https://github.com/0xBEEFCAF3/rust-payjoin.git", rev = "619ea5b7dac311461f5ba0d7e6f8ba2e7030e57d", features = ["v2", "io"]} -# payjoin = { path = "../../../rust-payjoin/payjoin", features = ["v2", "io"]} +payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index 07bdd2663..1784512c4 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -460,8 +460,10 @@ fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 log::info!("Finalizing input at: {}", index), + Err(_) => log::info!("Failed to finalizing input at: {}", index), + } } for index in witness_utxo_to_clean { @@ -492,14 +494,13 @@ fn process_proposal_psbt( descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) -> Result { - let desc = descs.first().unwrap(); - let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); if let Some((_, coin)) = coins.iter().next() { let txs = db_conn.list_wallet_transactions(&[coin.outpoint.txid]); let (db_tx, _, _) = txs.first().unwrap(); let tx = db_tx.clone(); + let txin = TxIn { previous_output: coin.outpoint, // TODO(arturgontijo): Avoiding Validation(ValidationError(Proposal(MixedSequence))) @@ -509,23 +510,26 @@ fn process_proposal_psbt( let txout = tx.tx_out(coin.outpoint.vout as usize).unwrap().clone(); - let derived = desc.derive(coin.derivation_index, secp); - let mut psbtin = Input { non_witness_utxo: Some(tx.clone()), witness_utxo: Some(txout.clone()), ..Default::default() }; - derived.update_psbt_in(&mut psbtin); + + // descs must always have 2 descriptors + assert_eq!(descs.len(), 2); + + let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); + receiver_derived_desc.update_psbt_in(&mut psbtin); + + let change_derived_desc = descs[1].derive(coin.derivation_index, secp); + let script_pubkey = change_derived_desc.script_pubkey(); let proposal = proposal_dummy_checks_bypass(&proposal); let proposal = proposal.commit_outputs(); - let proposa = proposal.commit_inputs(); + let proposal = proposal.commit_inputs(); - // let inputs = vec![InputPair::new(txin.clone(), psbtin.clone()).unwrap()]; - // let provisional_proposal = proposal.contribute_inputs(inputs).unwrap().commit_inputs(); - - let proposal = proposa + let proposal = proposal .finalize_proposal( |psbt| Ok(psbt.clone()), None, @@ -540,7 +544,7 @@ fn process_proposal_psbt( let output = TxOut { value: coin.amount, - script_pubkey: derived.script_pubkey(), + script_pubkey, }; psbt.outputs.push(Output::default()); psbt.unsigned_tx.output.push(output); @@ -553,7 +557,6 @@ fn process_proposal_psbt( pub fn payjoin_receiver_check( db_conn: &mut Box, - _bitcoind: &mut impl BitcoinInterface, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) { @@ -576,6 +579,7 @@ pub fn payjoin_receiver_check( .expect("Failed to process response"); if let Some(proposal) = proposal { log::info!("[Payjoin] receiver got a proposal..."); + let new_psbt = process_proposal_psbt(&proposal, db_conn, descs, secp).unwrap(); @@ -623,8 +627,6 @@ pub fn payjoin_receiver_check( } if is_signed { - let mut psbt = psbt.clone(); - let (req, ctx) = receiver .extract_req( Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"), @@ -641,6 +643,7 @@ pub fn payjoin_receiver_check( if let Some(proposal) = proposal { log::info!("[Payjoin] receiver got a proposal..."); + let mut psbt = psbt.clone(); finalize_psbt(&mut psbt, secp); let proposal = proposal_dummy_checks_bypass(&proposal); @@ -703,30 +706,25 @@ pub fn payjoin_receiver_check( } } -fn payjoin_sender_check( - db_conn: &mut Box, - secp: &secp256k1::Secp256k1, -) { +fn payjoin_sender_check(db_conn: &mut Box) { let payjoin_senders = db_conn.get_all_payjoin_senders(); for (bip21, txid, _) in payjoin_senders { log::info!("Payjoin sender: {}", bip21); - let mut psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); + let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); let pj_uri = Uri::try_from(bip21.as_str()) .expect("Invalid BIP21") .assume_checked() .check_pj_supported() .expect("Invalid PJ BIP21"); - // Clone the psbt to finalize, and copy over the witness, - // this is bc finalize mut will remove those fields and we need them again when we sign the pj psbt - let mut psbt_to_finalize = psbt.clone(); - psbt_to_finalize - .finalize_mut(secp) - .expect("finalize should work"); - - for (i, input) in psbt_to_finalize.inputs.iter_mut().enumerate() { - psbt.inputs[i].final_script_sig = input.final_script_sig.clone(); - psbt.inputs[i].final_script_witness = input.final_script_witness.clone(); + // TODO(arturgontijo): PDK removes these fields but we need them so GUI can properly sign the inputs + let mut input_fields_to_restore = vec![]; + for (index, input) in psbt.inputs.iter().enumerate() { + input_fields_to_restore.push(( + index, + input.witness_script.clone(), + input.bip32_derivation.clone(), + )); } let new_sender = SenderBuilder::new(psbt, pj_uri) @@ -759,7 +757,7 @@ fn payjoin_sender_check( Ok(resp) => { log::info!("Payjoin sender got a response..."); - let psbt = match get_ctx.process_response( + let mut psbt = match get_ctx.process_response( resp.bytes().expect("Failed to read response").as_ref(), ohttp_ctx, ) { @@ -775,6 +773,13 @@ fn payjoin_sender_check( continue; } }; + + // TODO(arturgontijo): Restoring witness_scripts and bip32_bip32_derivation so GUI can sign them + for (index, witness_script, bip32_derivation) in input_fields_to_restore { + psbt.inputs[index].witness_script = witness_script; + psbt.inputs[index].bip32_derivation = bip32_derivation; + } + // Store updated Payjoin psbt log::info!( "Updated Payjoin psbt: {} -> {}", @@ -782,7 +787,8 @@ fn payjoin_sender_check( psbt.unsigned_tx.compute_txid() ); db_conn.store_spend(&psbt); - log::info!("Deleting Payjoin psbt (txid={})", txid); + + log::info!("Deleting original Payjoin psbt (txid={})", txid); db_conn.delete_spend(&txid); // Mark the sender as completed @@ -814,8 +820,8 @@ pub fn poll( let mut db_conn = db.connection(); updates(&mut db_conn, bit, descs, secp); rescan_check(&mut db_conn, bit, descs, secp); - payjoin_sender_check(&mut db_conn, secp); - payjoin_receiver_check(&mut db_conn, bit, descs, secp); + payjoin_sender_check(&mut db_conn); + payjoin_receiver_check(&mut db_conn, descs, secp); let now: u32 = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .expect("current system time must be later than epoch") diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 7a34824cb..94e52ffa4 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -851,22 +851,11 @@ impl DaemonControl { .spend_tx(txid) .ok_or(CommandError::UnknownSpend(*txid))?; - let mut inputs_to_finalize = vec![]; - for (index, input) in spend_psbt.inputs.iter().enumerate() { - if input.final_script_sig.is_some() - || input.final_script_witness.is_some() - || input.partial_sigs.is_empty() - { - continue; + for index in 0..spend_psbt.inputs.len() { + match spend_psbt.finalize_inp_mut(&self.secp, index) { + Ok(_) => log::info!("Finalizing input at: {}", index), + Err(_) => log::info!("Failed to finalizing input at: {}", index), } - inputs_to_finalize.push(index); - } - - for index in inputs_to_finalize { - log::info!("Finalizing input at: {}", index); - spend_psbt - .finalize_inp_mut(&self.secp, index) - .map_err(|e| CommandError::SpendFinalization(e.to_string()))?; } // Then, broadcast it (or try to, we never know if we are not going to hit an From adad5df2c5668c14ca972d61603f3f5b703686d3 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Wed, 14 May 2025 19:49:47 -0300 Subject: [PATCH 03/72] [PDK] Stable sender workflow. --- lianad/src/bitcoin/poller/looper.rs | 250 +++++++++++++++------------ lianad/src/database/mod.rs | 14 +- lianad/src/database/sqlite/mod.rs | 55 ++++-- lianad/src/database/sqlite/schema.rs | 5 +- lianad/src/testutils.rs | 4 +- 5 files changed, 197 insertions(+), 131 deletions(-) diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index 1784512c4..f239fdfef 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -462,7 +462,7 @@ fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 log::info!("Finalizing input at: {}", index), - Err(_) => log::info!("Failed to finalizing input at: {}", index), + Err(e) => log::warn!("Failed to finalize input at: {} | {}", index, e), } } @@ -496,6 +496,12 @@ fn process_proposal_psbt( ) -> Result { let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); if let Some((_, coin)) = coins.iter().next() { + // descs must always have 2 descriptors + assert_eq!(descs.len(), 2); + + let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); + let script_pubkey = receiver_derived_desc.script_pubkey(); + let txs = db_conn.list_wallet_transactions(&[coin.outpoint.txid]); let (db_tx, _, _) = txs.first().unwrap(); @@ -508,7 +514,10 @@ fn process_proposal_psbt( ..Default::default() }; - let txout = tx.tx_out(coin.outpoint.vout as usize).unwrap().clone(); + let txout = TxOut { + value: coin.amount, + script_pubkey, + }; let mut psbtin = Input { non_witness_utxo: Some(tx.clone()), @@ -516,15 +525,8 @@ fn process_proposal_psbt( ..Default::default() }; - // descs must always have 2 descriptors - assert_eq!(descs.len(), 2); - - let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); receiver_derived_desc.update_psbt_in(&mut psbtin); - let change_derived_desc = descs[1].derive(coin.derivation_index, secp); - let script_pubkey = change_derived_desc.script_pubkey(); - let proposal = proposal_dummy_checks_bypass(&proposal); let proposal = proposal.commit_outputs(); let proposal = proposal.commit_inputs(); @@ -542,12 +544,8 @@ fn process_proposal_psbt( psbt.inputs.push(psbtin); psbt.unsigned_tx.input.push(txin); - let output = TxOut { - value: coin.amount, - script_pubkey, - }; psbt.outputs.push(Output::default()); - psbt.unsigned_tx.output.push(output); + psbt.unsigned_tx.output.push(txout); return Ok(psbt); } @@ -562,10 +560,9 @@ pub fn payjoin_receiver_check( ) { let mut payjoin_receivers = db_conn.get_all_payjoin_receivers(); for (address, status, receiver, psbt) in payjoin_receivers.iter_mut() { - log::info!("[Payjoin] PayjoinReceiverStatus: {:?}", status); match status { PayjoinReceiverStatus::Pending => { - log::info!("[Payjoin] receiver: {}", receiver.pj_uri()); + log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); let (req, ctx) = receiver .extract_req(OHTTP_RELAY) .expect("Failed to extract request"); @@ -603,6 +600,7 @@ pub fn payjoin_receiver_check( } } PayjoinReceiverStatus::Signing => { + log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); let psbt = match Psbt::from_str(psbt) { Ok(psbt) => psbt, Err(err) => { @@ -707,105 +705,143 @@ pub fn payjoin_receiver_check( } fn payjoin_sender_check(db_conn: &mut Box) { + let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); let payjoin_senders = db_conn.get_all_payjoin_senders(); - for (bip21, txid, _) in payjoin_senders { - log::info!("Payjoin sender: {}", bip21); - let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); - let pj_uri = Uri::try_from(bip21.as_str()) - .expect("Invalid BIP21") - .assume_checked() - .check_pj_supported() - .expect("Invalid PJ BIP21"); - - // TODO(arturgontijo): PDK removes these fields but we need them so GUI can properly sign the inputs - let mut input_fields_to_restore = vec![]; - for (index, input) in psbt.inputs.iter().enumerate() { - input_fields_to_restore.push(( - index, - input.witness_script.clone(), - input.bip32_derivation.clone(), - )); - } - - let new_sender = SenderBuilder::new(psbt, pj_uri) - .build_recommended(bitcoin::FeeRate::BROADCAST_MIN) - .expect("Failed to build sender"); - - // TODO: should just be able to load a sender from the db, and not use the NoopPersister. - let sender_storage_token = new_sender - .persist(&mut NoopPersister) - .expect("Failed to persist sender"); - - let sender = - Sender::load(sender_storage_token, &NoopPersister).expect("Failed to load sender"); - - let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); - let (post_req, post_ctx) = sender.extract_v2(ohttp_url).expect("Failed to extract v2"); - // Send original PSBT to the receiver via the BIP77 directory - match post_request(post_req.clone()) { - Ok(resp) => { - let get_ctx = post_ctx - .process_response(resp.bytes().expect("Failed to read response").as_ref()) - .expect("Failed to process response"); - - // Read the response from the receiver via the BIP77 directory - let (get_req, ohttp_ctx) = get_ctx - .extract_req(OHTTP_RELAY) - .expect("Failed to extract get request"); - - match post_request(get_req.clone()) { - Ok(resp) => { - log::info!("Payjoin sender got a response..."); - - let mut psbt = match get_ctx.process_response( - resp.bytes().expect("Failed to read response").as_ref(), - ohttp_ctx, - ) { - Ok(Some(psbt)) => psbt, - Ok(None) => { - // nothing to do yet, no response - log::warn!("Nothing to do yet, no response..."); - continue; - } - Err(e) => { - log::warn!("Failed to process payjoin sender response: {:?}", e); - // TODO: handle error - continue; - } - }; - - // TODO(arturgontijo): Restoring witness_scripts and bip32_bip32_derivation so GUI can sign them - for (index, witness_script, bip32_derivation) in input_fields_to_restore { - psbt.inputs[index].witness_script = witness_script; - psbt.inputs[index].bip32_derivation = bip32_derivation; - } - - // Store updated Payjoin psbt - log::info!( - "Updated Payjoin psbt: {} -> {}", + for (bip21, txid, status, maybe_sender) in payjoin_senders { + match status { + PayjoinSenderStatus::Pending => { + log::info!("[Payjoin] PayjoinSenderStatus: {:?} | {}", status, bip21); + + let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); + + let pj_uri = Uri::try_from(bip21.as_str()) + .expect("Invalid BIP21") + .assume_checked() + .check_pj_supported() + .expect("Invalid PJ BIP21"); + + let new_sender = SenderBuilder::new(psbt, pj_uri) + .build_recommended(bitcoin::FeeRate::BROADCAST_MIN) + .expect("Failed to build sender"); + + // TODO: should just be able to load a sender from the db, and not use the NoopPersister. + let storage_token = new_sender + .persist(&mut NoopPersister) + .expect("Failed to persist sender"); + + let sender = + Sender::load(storage_token, &NoopPersister).expect("Failed to load sender"); + + let (post_req, _) = sender + .extract_v2(ohttp_url.clone()) + .expect("Failed to extract v2"); + // Send original PSBT to the receiver via the BIP77 directory + match post_request(post_req.clone()) { + Ok(_) => { + log::info!("[Payjoin] Updating PSBT's STATUS..."); + db_conn.update_payjoin_sender_status( txid, - psbt.unsigned_tx.compute_txid() + PayjoinSenderStatus::WaitingReceiver, + Some(sender), ); - db_conn.store_spend(&psbt); + } + Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), + } + } + PayjoinSenderStatus::WaitingReceiver => { + log::info!( + "[Payjoin] PayjoinSenderStatus: {:?} | sender_is_set={}", + status, + maybe_sender.is_some() + ); + if let Some(sender) = maybe_sender { + let (post_req, post_ctx) = sender + .extract_v2(ohttp_url.clone()) + .expect("Failed to extract v2"); + + match post_request(post_req.clone()) { + Ok(resp) => { + let get_ctx = post_ctx + .process_response( + resp.bytes().expect("Must be valid response").as_ref(), + ) + .expect("Failed to process response"); + + // Read the response from the receiver via the BIP77 directory + let (get_req, ohttp_ctx) = get_ctx + .extract_req(OHTTP_RELAY) + .expect("Failed to extract get request"); + + let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); + + // TODO(arturgontijo): PDK removes these fields but we need them so GUI can properly sign the inputs + let mut input_fields_to_restore = vec![]; + for (index, input) in psbt.inputs.iter().enumerate() { + input_fields_to_restore.push(( + index, + input.witness_script.clone(), + input.bip32_derivation.clone(), + )); + } - log::info!("Deleting original Payjoin psbt (txid={})", txid); - db_conn.delete_spend(&txid); + match post_request(get_req.clone()) { + Ok(resp) => { + log::info!("Payjoin sender got a final PSBT..."); + + let mut psbt = match get_ctx.process_response( + resp.bytes().expect("Failed to read response").as_ref(), + ohttp_ctx, + ) { + Ok(Some(psbt)) => psbt, + Ok(None) => { + // nothing to do yet, no response + log::warn!("Nothing to do yet, no response..."); + continue; + } + Err(e) => { + log::warn!( + "Failed to process payjoin sender response: {:?}", + e + ); + // TODO: handle error + continue; + } + }; + + // TODO(arturgontijo): Restoring witness_scripts and bip32_bip32_derivation so GUI can sign them + for (index, witness_script, bip32_derivation) in + input_fields_to_restore + { + psbt.inputs[index].witness_script = witness_script; + psbt.inputs[index].bip32_derivation = bip32_derivation; + } - // Mark the sender as completed - db_conn.update_payjoin_sender_status(txid, PayjoinSenderStatus::Completed); + // Store updated Payjoin psbt + log::info!( + "Updated Payjoin psbt: {} -> {}", + txid, + psbt.unsigned_tx.compute_txid() + ); + db_conn.store_spend(&psbt); + + log::info!("Deleting original Payjoin psbt (txid={})", txid); + db_conn.delete_spend(&txid); + + // Mark the sender as completed + db_conn.update_payjoin_sender_status( + txid, + PayjoinSenderStatus::Completed, + None, + ); + } + Err(e) => log::warn!("Failed to get receiver's proposal: {:?}", e), + } + } + Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), } - Err(err) => log::error!( - "payjoin_sender_check(getting_psbt): {} -> {}", - get_req.url, - err - ), } } - Err(err) => log::error!( - "payjoin_sender_check(sending_og_psbt): {} -> {}", - post_req.url, - err - ), + _ => {} } } } diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index fde66d5b5..3c59d7978 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -22,7 +22,7 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; -use payjoin::receive::v2::Receiver; +use payjoin::{receive::v2::Receiver, send::v2::Sender}; use sqlite::{PayjoinReceiverStatus, PayjoinSenderStatus}; /// Information about the wallet. @@ -221,12 +221,15 @@ pub trait DatabaseConnection { /// Create a payjoin sender fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid); /// Get a all active payjoin senders - fn get_all_payjoin_senders(&mut self) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus)>; + fn get_all_payjoin_senders( + &mut self, + ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)>; /// Update the status of a payjoin sender fn update_payjoin_sender_status( &mut self, spend_tx_id: bitcoin::Txid, status: PayjoinSenderStatus, + maybe_sender: Option, ); // ------- @@ -481,7 +484,9 @@ impl DatabaseConnection for SqliteConn { self.create_payjoin_sender(bip21, spend_tx_id) } - fn get_all_payjoin_senders(&mut self) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus)> { + fn get_all_payjoin_senders( + &mut self, + ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { self.get_all_payjoin_senders() } @@ -489,8 +494,9 @@ impl DatabaseConnection for SqliteConn { &mut self, spend_tx_id: bitcoin::Txid, status: PayjoinSenderStatus, + sender: Option, ) { - self.update_payjoin_sender_status(spend_tx_id, status) + self.update_payjoin_sender_status(spend_tx_id, status, sender) } } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 8604fd3ce..ad05a5a44 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -27,7 +27,7 @@ use crate::{ }, }; use liana::descriptors::LianaDescriptor; -use payjoin::receive::v2::Receiver; +use payjoin::{receive::v2::Receiver, send::v2::Sender}; use std::{ cmp, @@ -218,7 +218,7 @@ impl SqliteDb { #[derive(Debug, Clone, Copy, PartialEq)] pub enum PayjoinSenderStatus { Pending = 0, - Signing = 1, + WaitingReceiver = 1, Completed = 2, // TODO: more specific enums for why it failed Failed = 3, @@ -237,7 +237,7 @@ impl From for PayjoinSenderStatus { fn from(status: i32) -> Self { match status { 0 => PayjoinSenderStatus::Pending, - 1 => PayjoinSenderStatus::Signing, + 1 => PayjoinSenderStatus::WaitingReceiver, 2 => PayjoinSenderStatus::Completed, 3 => PayjoinSenderStatus::Failed, _ => panic!("Invalid payjoin sender status: {}", status), @@ -1075,26 +1075,34 @@ impl SqliteConn { let txid = spend_tx_id[..].to_vec(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_senders (bip21, spend_tx_id, status) VALUES (?1, ?2, ?3)", - rusqlite::params![bip21, txid, status as i32], + "INSERT INTO payjoin_senders (bip21, spend_tx_id, status, sender) VALUES (?1, ?2, ?3, ?4)", + rusqlite::params![bip21, txid, status as i32, String::new()], )?; Ok(()) }) .expect("Db must not fail"); } - pub fn get_all_payjoin_senders(&mut self) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus)> { + pub fn get_all_payjoin_senders( + &mut self, + ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { db_query( &mut self.conn, - "SELECT bip21, spend_tx_id, status FROM payjoin_senders WHERE status = ?1", - rusqlite::params![PayjoinSenderStatus::Pending as i32], + "SELECT bip21, spend_tx_id, status, sender FROM payjoin_senders", + rusqlite::params![], |row| { let bip21: String = row.get(0)?; let spend_tx_id: Vec = row.get(1)?; let txid: bitcoin::Txid = encode::deserialize(&spend_tx_id).expect("We only store valid txids"); let status: i32 = row.get(2)?; - Ok((bip21, txid, PayjoinSenderStatus::from(status))) + let sender_json: String = row.get(3)?; + let maybe_sender: Option = if !sender_json.is_empty() { + Some(serde_json::from_str(&sender_json).unwrap()) + } else { + None + }; + Ok((bip21, txid, PayjoinSenderStatus::from(status), maybe_sender)) }, ) .expect("Db must not fail") @@ -1104,15 +1112,28 @@ impl SqliteConn { &mut self, spend_tx_id: bitcoin::Txid, status: PayjoinSenderStatus, + maybe_sender: Option, ) { - db_exec(&mut self.conn, |db_tx| { - db_tx.execute( - "UPDATE payjoin_senders SET status = ?1 WHERE spend_tx_id = ?2", - rusqlite::params![status as i32, spend_tx_id[..].to_vec()], - )?; - Ok(()) - }) - .expect("Db must not fail"); + if let Some(sender) = maybe_sender { + let sender_json = serde_json::to_string(&sender).unwrap(); + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_senders SET status = ?1, sender = ?2 WHERE spend_tx_id = ?3", + rusqlite::params![status as i32, sender_json, spend_tx_id[..].to_vec()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } else { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_senders SET status = ?1 WHERE spend_tx_id = ?2", + rusqlite::params![status as i32, spend_tx_id[..].to_vec()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } } } diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index c4cad306f..e93fc5a30 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -128,14 +128,15 @@ CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, bip21 TEXT NOT NULL, spend_tx_id BLOB UNIQUE NOT NULL, - status INTEGER NOT NULL CHECK (status IN (0,1,2,3,4)) + status INTEGER NOT NULL CHECK (status IN (0,1,2,3)), + sender TEXT NOT NULL ); /* Payjoin receivers */ CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, address TEXT UNIQUE NOT NULL, - status INTEGER NOT NULL CHECK (status IN (0,1,2,3,4)), + status INTEGER NOT NULL CHECK (status IN (0,1,2,3)), receiver TEXT NOT NULL, psbt TEXT NOT NULL ); diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index d4abf93c4..8d6dc6a00 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -9,7 +9,7 @@ use crate::{ DaemonControl, DaemonHandle, }; use liana::descriptors; -use payjoin::receive::v2::Receiver; +use payjoin::{receive::v2::Receiver, send::v2::Sender}; use std::convert::TryInto; use std::{ @@ -565,6 +565,7 @@ impl DatabaseConnection for DummyDatabase { String, bitcoin::Txid, crate::database::sqlite::PayjoinSenderStatus, + Option, )> { todo!() } @@ -573,6 +574,7 @@ impl DatabaseConnection for DummyDatabase { &mut self, _spend_tx_id: bitcoin::Txid, _status: crate::database::sqlite::PayjoinSenderStatus, + _maybe_sender: Option, ) { todo!() } From 097f4a16d7d97896f502eaf03926849c01ecbed7 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Thu, 15 May 2025 09:52:46 -0300 Subject: [PATCH 04/72] [PDK] payjoin mod. --- liana-gui/src/app/state/psbt.rs | 8 + liana-gui/src/app/view/psbt.rs | 48 ++- liana-gui/src/daemon/model.rs | 1 + liana-ui/src/color.rs | 6 + liana-ui/src/theme/palette.rs | 2 + liana-ui/src/theme/text.rs | 6 + lianad/src/bitcoin/mod.rs | 1 + lianad/src/bitcoin/payjoin/helpers.rs | 81 +++++ lianad/src/bitcoin/payjoin/mod.rs | 3 + lianad/src/bitcoin/payjoin/receiver.rs | 229 ++++++++++++ lianad/src/bitcoin/payjoin/sender.rs | 157 +++++++++ lianad/src/bitcoin/poller/looper.rs | 460 +------------------------ 12 files changed, 535 insertions(+), 467 deletions(-) create mode 100644 lianad/src/bitcoin/payjoin/helpers.rs create mode 100644 lianad/src/bitcoin/payjoin/mod.rs create mode 100644 lianad/src/bitcoin/payjoin/receiver.rs create mode 100644 lianad/src/bitcoin/payjoin/sender.rs diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index f4c73c5e9..aaf9462c1 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -556,6 +556,14 @@ impl Modal for SignModal { self.signed.insert(fingerprint); let daemon = daemon.clone(); merge_signatures(&mut tx.psbt, &psbt); + + // TODO(arturgontijo): Use better design. Maybe checking for foreign inputs. + // Payjoin Receiver Side + let psbt_ready = psbt.clone().extract_tx(); + if tx.bip21.is_none() && psbt_ready.is_err() { + tx.status = SpendStatus::PayjoinProposalReady; + } + if self.is_saved { return Task::perform( async move { daemon.update_spend_tx(&psbt).await.map_err(|e| e.into()) }, diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index c41600c54..d45e73e35 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -423,19 +423,41 @@ pub fn signatures<'a>( ) -> Element<'a, Message> { Column::new() .push(if tx.status == SpendStatus::PayjoinInitiated { - Container::new(scrollable( - Row::new() - .spacing(5) - .align_y(Alignment::Center) - .spacing(10) - .push(p1_bold("Status")) - .push(icon::circle_check_icon().style(theme::text::warning)) - .push( - text(" Payjoin initiated") - .bold() - .style(theme::text::warning), - ), - )) + Container::new( + scrollable( + Row::new() + .spacing(5) + .align_y(Alignment::Center) + .spacing(10) + .push(p1_bold("Status")) + .push(icon::circle_check_icon().style(theme::text::payjoin)) + .push(text("Payjoin Initiated").bold().style(theme::text::payjoin)), + ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new().width(2).scroller_width(2), + )), + ) + .padding(15) + } else if tx.status == SpendStatus::PayjoinProposalReady { + Container::new( + scrollable( + Row::new() + .spacing(5) + .align_y(Alignment::Center) + .spacing(10) + .push(p1_bold("Status")) + .push(icon::circle_check_icon().style(theme::text::payjoin)) + .push( + text("Payjoin Proposal Ready") + .bold() + .style(theme::text::payjoin), + ), + ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::new().width(2).scroller_width(2), + )), + ) + .padding(15) } else if let Some(sigs) = tx.path_ready() { Container::new( scrollable( diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index befe72869..9d9ec7a0e 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -62,6 +62,7 @@ pub enum SpendStatus { Spent, Deprecated, PayjoinInitiated, + PayjoinProposalReady, } impl SpendTx { diff --git a/liana-ui/src/color.rs b/liana-ui/src/color.rs index 54b9166e4..54c4540e8 100644 --- a/liana-ui/src/color.rs +++ b/liana-ui/src/color.rs @@ -61,3 +61,9 @@ pub const BLUE: Color = Color::from_rgb( 0xD3 as f32 / 255.0, 0xFC as f32 / 255.0, ); + +pub const PAYJOIN_PINK: Color = Color::from_rgb( + 0xC7 as f32 / 255.0, + 0x15 as f32 / 255.0, + 0x85 as f32 / 255.0, +); diff --git a/liana-ui/src/theme/palette.rs b/liana-ui/src/theme/palette.rs index 1833c36b3..737cc59b9 100644 --- a/liana-ui/src/theme/palette.rs +++ b/liana-ui/src/theme/palette.rs @@ -25,6 +25,7 @@ pub struct Text { pub warning: iced::Color, pub success: iced::Color, pub error: iced::Color, + pub payjoin: iced::Color, } #[derive(Debug, Copy, Clone, PartialEq)] @@ -175,6 +176,7 @@ impl std::default::Default for Palette { warning: color::ORANGE, success: color::GREEN, error: color::RED, + payjoin: color::PAYJOIN_PINK, }, buttons: Buttons { primary: Button { diff --git a/liana-ui/src/theme/text.rs b/liana-ui/src/theme/text.rs index 23b2977ef..2df121acb 100644 --- a/liana-ui/src/theme/text.rs +++ b/liana-ui/src/theme/text.rs @@ -57,3 +57,9 @@ pub fn error(theme: &Theme) -> Style { pub fn custom(color: iced::Color) -> Style { Style { color: Some(color) } } + +pub fn payjoin(theme: &Theme) -> Style { + Style { + color: Some(theme.colors.text.payjoin), + } +} diff --git a/lianad/src/bitcoin/mod.rs b/lianad/src/bitcoin/mod.rs index 26e755df8..c20e7006f 100644 --- a/lianad/src/bitcoin/mod.rs +++ b/lianad/src/bitcoin/mod.rs @@ -4,6 +4,7 @@ pub mod d; pub mod electrum; +pub mod payjoin; pub mod poller; use crate::bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry}; diff --git a/lianad/src/bitcoin/payjoin/helpers.rs b/lianad/src/bitcoin/payjoin/helpers.rs new file mode 100644 index 000000000..1d1b2a526 --- /dev/null +++ b/lianad/src/bitcoin/payjoin/helpers.rs @@ -0,0 +1,81 @@ +use std::error::Error; + +use miniscript::{ + bitcoin::{secp256k1, Psbt, ScriptBuf, TxOut}, + psbt::PsbtExt, +}; + +use payjoin::{ + bitcoin::Amount, + receive::v2::{UncheckedProposal, WantsOutputs}, +}; + +pub const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; + +pub fn http_agent() -> reqwest::blocking::Client { + reqwest::blocking::Client::new() +} + +pub fn post_request(req: payjoin::Request) -> Result> { + let http = http_agent(); + match http + .post(req.url) + .header("Content-Type", req.content_type) + .body(req.body) + .send() + { + Ok(r) => Ok(r), + Err(err) => Err(format!("Failed to post_reques(): {}", err).into()), + } +} + +pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1) { + let mut witness_utxo_to_clean = vec![]; + let mut inputs_to_finalize = vec![]; + for (index, input) in psbt.inputs.iter_mut().enumerate() { + if input.witness_utxo.is_none() { + // finalize_proposal() cleans this up, but we need it to finalize_inp_mut() bellow + input.witness_utxo = Some(TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::default(), + }); + witness_utxo_to_clean.push(index); + continue; + } + if input.final_script_sig.is_some() + || input.final_script_witness.is_some() + || input.partial_sigs.is_empty() + { + continue; + } + inputs_to_finalize.push(index); + } + + for index in inputs_to_finalize { + match psbt.finalize_inp_mut(&secp, index) { + Ok(_) => log::info!("Finalizing input at: {}", index), + Err(e) => log::warn!("Failed to finalize input at: {} | {}", index, e), + } + } + + for index in witness_utxo_to_clean { + psbt.inputs[index].witness_utxo = None; + } +} + +pub fn proposal_dummy_checks_bypass(proposal: &UncheckedProposal) -> WantsOutputs { + let proposal = proposal + .clone() + .check_broadcast_suitability(None, |_| Ok(true)) + .expect("Failed to check broadcast suitability"); + let proposal = proposal + .check_inputs_not_owned(|_input| Ok(false)) + .expect("Failed to check inputs not owned"); + let proposal = proposal + .check_no_inputs_seen_before(|_| Ok(false)) + .expect("Failed to check no inputs seen before"); + let proposal = proposal + .identify_receiver_outputs(|_| Ok(true)) + .expect("Failed to identify receiver outputs"); + proposal +} diff --git a/lianad/src/bitcoin/payjoin/mod.rs b/lianad/src/bitcoin/payjoin/mod.rs new file mode 100644 index 000000000..782d782ad --- /dev/null +++ b/lianad/src/bitcoin/payjoin/mod.rs @@ -0,0 +1,3 @@ +mod helpers; +pub mod receiver; +pub mod sender; diff --git a/lianad/src/bitcoin/payjoin/receiver.rs b/lianad/src/bitcoin/payjoin/receiver.rs new file mode 100644 index 000000000..88357a47c --- /dev/null +++ b/lianad/src/bitcoin/payjoin/receiver.rs @@ -0,0 +1,229 @@ +use crate::{ + bitcoin::payjoin::helpers::{ + finalize_psbt, post_request, proposal_dummy_checks_bypass, OHTTP_RELAY, + }, + database::{sqlite::PayjoinReceiverStatus, CoinStatus, DatabaseConnection}, +}; + +use std::str::FromStr; + +use liana::descriptors; + +use payjoin::{ + bitcoin::{ + psbt::{Input, Output}, + secp256k1, FeeRate, Psbt, Sequence, TxIn, + }, + receive::v2::UncheckedProposal, + Url, +}; + +pub fn process_proposal_psbt( + proposal: &UncheckedProposal, + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result { + let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); + if let Some((_, coin)) = coins.iter().next() { + let proposal = proposal_dummy_checks_bypass(&proposal); + let proposal = proposal.commit_outputs(); + let proposal = proposal.commit_inputs(); + + let proposal = proposal + .finalize_proposal( + |psbt| Ok(psbt.clone()), + None, + Some(FeeRate::from_sat_per_vb(150).unwrap()), + ) + .expect("Failed to finalize proposal"); + + let mut psbt = proposal.psbt().clone(); + + // descs must always have 2 descriptors + assert_eq!(descs.len(), 2); + + let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); + + let txs = db_conn.list_wallet_transactions(&[coin.outpoint.txid]); + let (db_tx, _, _) = txs.first().unwrap(); + + let tx = db_tx.clone(); + + let txout = tx.tx_out(coin.outpoint.vout as usize).unwrap().clone(); + + let mut psbtin = Input { + non_witness_utxo: Some(tx.clone()), + witness_utxo: Some(txout.clone()), + ..Default::default() + }; + + receiver_derived_desc.update_psbt_in(&mut psbtin); + + let txin = TxIn { + previous_output: coin.outpoint, + // TODO(arturgontijo): Avoiding Validation(ValidationError(Proposal(MixedSequence))) + sequence: Sequence::from_hex("0xFFFFFFFD").unwrap(), + ..Default::default() + }; + + psbt.inputs.push(psbtin); + psbt.unsigned_tx.input.push(txin); + + psbt.outputs.push(Output::default()); + psbt.unsigned_tx.output.push(txout); + + return Ok(psbt); + } + + Err(()) +} + +pub fn payjoin_receiver_check( + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) { + let mut payjoin_receivers = db_conn.get_all_payjoin_receivers(); + for (address, status, receiver, psbt) in payjoin_receivers.iter_mut() { + match status { + PayjoinReceiverStatus::Pending => { + log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); + let (req, ctx) = receiver + .extract_req(OHTTP_RELAY) + .expect("Failed to extract request"); + match post_request(req.clone()) { + Ok(resp) => { + let proposal = receiver + .process_res( + resp.bytes().expect("Failed to read response").as_ref(), + ctx, + ) + .expect("Failed to process response"); + if let Some(proposal) = proposal { + log::info!("[Payjoin] receiver got a proposal..."); + + let new_psbt = + process_proposal_psbt(&proposal, db_conn, descs, secp).unwrap(); + + // DB logic + log::info!("[Payjoin] Inserting PSBT and STATUS..."); + db_conn.update_payjoin_receiver_status( + &address, + PayjoinReceiverStatus::Signing, + new_psbt.to_string(), + ); + db_conn.store_spend(&new_psbt); + + log::info!("[Payjoin] PSBT in DB: {}", new_psbt.to_string()); + } else { + log::info!("empty proposal") + } + } + Err(err) => { + log::warn!("[Payjoin] payjoin_receiver_check(): {} -> {}", req.url, err) + } + } + } + PayjoinReceiverStatus::Signing => { + log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); + let psbt = match Psbt::from_str(psbt) { + Ok(psbt) => psbt, + Err(err) => { + log::error!( + "[Payjoin] payjoin_receiver_check(psbt_from): len={} -> {}", + psbt.len(), + err + ); + continue; + } + }; + let txid = psbt.unsigned_tx.compute_txid(); + match db_conn.spend_tx(&txid) { + Some(psbt) => { + let mut is_signed = false; + for psbtin in &psbt.inputs { + if !psbtin.partial_sigs.is_empty() { + log::info!("PSBT was signed!"); + is_signed = true; + break; + } + } + + if is_signed { + let (req, ctx) = receiver + .extract_req( + Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"), + ) + .expect("Failed to extract request"); + match post_request(req.clone()) { + Ok(resp) => { + let proposal = receiver + .process_res( + resp.bytes().expect("Failed to read response").as_ref(), + ctx, + ) + .expect("Failed to process response"); + if let Some(proposal) = proposal { + log::info!("[Payjoin] receiver got a proposal..."); + + let mut psbt = psbt.clone(); + finalize_psbt(&mut psbt, secp); + + let proposal = proposal_dummy_checks_bypass(&proposal); + let proposal = proposal.commit_outputs(); + let proposal = proposal.commit_inputs(); + + let mut proposal = proposal + .finalize_proposal( + |_| Ok(psbt.clone()), + None, + Some(FeeRate::from_sat_per_vb(150).unwrap()), + ) + .expect("Failed to finalize proposal"); + + let (req, ctx) = proposal + .extract_req( + Url::from_str(OHTTP_RELAY) + .expect("Invalid OHTTP relay"), + ) + .expect("Failed to extract request"); + + // Respond to sender + log::info!("[Payjoin] receiver responding to sender..."); + match post_request(req.clone()) { + Ok(resp) => { + let _proposal = receiver + .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) + .expect("Failed to process response"); + // Update status of receiver + db_conn.update_payjoin_receiver_status( + &address, + PayjoinReceiverStatus::Completed, + psbt.to_string(), + ); + }, + Err(err) => log::error!( + "[Payjoin] payjoin_receiver_check(respond_to_sender): {} -> {}", + req.url, err + ), + } + } else { + log::info!("empty proposal") + } + } + Err(err) => log::error!( + "[Payjoin] payjoin_receiver_check(Signed): {} -> {}", + req.url, + err + ), + } + } + } + None => {} + } + } + _ => {} + } + } +} diff --git a/lianad/src/bitcoin/payjoin/sender.rs b/lianad/src/bitcoin/payjoin/sender.rs new file mode 100644 index 000000000..53c314ded --- /dev/null +++ b/lianad/src/bitcoin/payjoin/sender.rs @@ -0,0 +1,157 @@ +use crate::{ + bitcoin::payjoin::helpers::post_request, + database::{sqlite::PayjoinSenderStatus, DatabaseConnection}, +}; + +use std::{convert::TryFrom, str::FromStr}; + +use payjoin::{ + bitcoin::FeeRate, + persist::NoopPersister, + send::v2::{Sender, SenderBuilder}, + Uri, UriExt, Url, +}; + +use super::helpers::OHTTP_RELAY; + +pub fn payjoin_sender_check(db_conn: &mut Box) { + let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); + let payjoin_senders = db_conn.get_all_payjoin_senders(); + for (bip21, txid, status, maybe_sender) in payjoin_senders { + match status { + PayjoinSenderStatus::Pending => { + log::info!("[Payjoin] PayjoinSenderStatus: {:?} | {}", status, bip21); + + let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); + + let pj_uri = Uri::try_from(bip21.as_str()) + .expect("Invalid BIP21") + .assume_checked() + .check_pj_supported() + .expect("Invalid PJ BIP21"); + + let new_sender = SenderBuilder::new(psbt, pj_uri) + .build_recommended(FeeRate::BROADCAST_MIN) + .expect("Failed to build sender"); + + // TODO: should just be able to load a sender from the db, and not use the NoopPersister. + let storage_token = new_sender + .persist(&mut NoopPersister) + .expect("Failed to persist sender"); + + let sender = + Sender::load(storage_token, &NoopPersister).expect("Failed to load sender"); + + let (post_req, _) = sender + .extract_v2(ohttp_url.clone()) + .expect("Failed to extract v2"); + // Send original PSBT to the receiver via the BIP77 directory + match post_request(post_req.clone()) { + Ok(_) => { + log::info!("[Payjoin] Updating PSBT's STATUS..."); + db_conn.update_payjoin_sender_status( + txid, + PayjoinSenderStatus::WaitingReceiver, + Some(sender), + ); + } + Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), + } + } + PayjoinSenderStatus::WaitingReceiver => { + log::info!( + "[Payjoin] PayjoinSenderStatus: {:?} | sender_is_set={}", + status, + maybe_sender.is_some() + ); + if let Some(sender) = maybe_sender { + let (post_req, post_ctx) = sender + .extract_v2(ohttp_url.clone()) + .expect("Failed to extract v2"); + + match post_request(post_req.clone()) { + Ok(resp) => { + let get_ctx = post_ctx + .process_response( + resp.bytes().expect("Must be valid response").as_ref(), + ) + .expect("Failed to process response"); + + // Read the response from the receiver via the BIP77 directory + let (get_req, ohttp_ctx) = get_ctx + .extract_req(OHTTP_RELAY) + .expect("Failed to extract get request"); + + let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); + + // TODO(arturgontijo): PDK removes these fields but we need them so GUI can properly sign the inputs + let mut input_fields_to_restore = vec![]; + for (index, input) in psbt.inputs.iter().enumerate() { + input_fields_to_restore.push(( + index, + input.witness_script.clone(), + input.bip32_derivation.clone(), + )); + } + + match post_request(get_req.clone()) { + Ok(resp) => { + log::info!("Payjoin sender got a final PSBT..."); + + let mut psbt = match get_ctx.process_response( + resp.bytes().expect("Failed to read response").as_ref(), + ohttp_ctx, + ) { + Ok(Some(psbt)) => psbt, + Ok(None) => { + // nothing to do yet, no response + log::warn!("Nothing to do yet, no response..."); + continue; + } + Err(e) => { + log::warn!( + "Failed to process payjoin sender response: {:?}", + e + ); + // TODO: handle error + continue; + } + }; + + // TODO(arturgontijo): Restoring witness_scripts and bip32_bip32_derivation so GUI can sign them + for (index, witness_script, bip32_derivation) in + input_fields_to_restore + { + psbt.inputs[index].witness_script = witness_script; + psbt.inputs[index].bip32_derivation = bip32_derivation; + } + + // Store updated Payjoin psbt + log::info!( + "Updated Payjoin psbt: {} -> {}", + txid, + psbt.unsigned_tx.compute_txid() + ); + db_conn.store_spend(&psbt); + + log::info!("Deleting original Payjoin psbt (txid={})", txid); + db_conn.delete_spend(&txid); + + // Mark the sender as completed + db_conn.update_payjoin_sender_status( + txid, + PayjoinSenderStatus::Completed, + None, + ); + } + Err(e) => log::warn!("Failed to get receiver's proposal: {:?}", e), + } + } + Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), + } + } + } + _ => {} + } + } +} diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index f239fdfef..7f8373e66 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -1,37 +1,15 @@ use crate::{ - bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress}, - database::{ - sqlite::{PayjoinReceiverStatus, PayjoinSenderStatus}, - Coin, CoinStatus, DatabaseConnection, DatabaseInterface, + bitcoin::{ + payjoin::{receiver::payjoin_receiver_check, sender::payjoin_sender_check}, + BitcoinInterface, BlockChainTip, UTxO, UTxOAddress, }, + database::{Coin, DatabaseConnection, DatabaseInterface}, }; -use std::{ - collections::HashSet, - convert::{TryFrom, TryInto}, - error::Error, - str::FromStr, - sync, thread, time, -}; +use std::{collections::HashSet, convert::TryInto, sync, thread, time}; use liana::descriptors; -// use log::{error, info, warn}; -use miniscript::{ - bitcoin::{self, secp256k1}, - psbt::PsbtExt, -}; -use payjoin::{ - bitcoin::{ - psbt::{Input, Output}, - Amount, Psbt, ScriptBuf, Sequence, TxIn, TxOut, - }, - persist::NoopPersister, - receive::v2::{UncheckedProposal, WantsOutputs}, - send::v2::{Sender, SenderBuilder}, - Uri, UriExt, Url, -}; - -const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; +use miniscript::bitcoin::{self, secp256k1}; #[derive(Debug, Clone)] struct UpdatedCoins { @@ -420,432 +398,6 @@ pub fn sync_poll_interval() -> time::Duration { time::Duration::from_secs(0) } -fn http_agent() -> reqwest::blocking::Client { - reqwest::blocking::Client::new() -} - -fn post_request(req: payjoin::Request) -> Result> { - let http = http_agent(); - match http - .post(req.url) - .header("Content-Type", req.content_type) - .body(req.body) - .send() - { - Ok(r) => Ok(r), - Err(err) => Err(format!("Failed to post_reques(): {}", err).into()), - } -} - -fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1) { - let mut witness_utxo_to_clean = vec![]; - let mut inputs_to_finalize = vec![]; - for (index, input) in psbt.inputs.iter_mut().enumerate() { - if input.witness_utxo.is_none() { - // finalize_proposal() cleans this up, but we need it to finalize_inp_mut() bellow - input.witness_utxo = Some(TxOut { - value: Amount::ZERO, - script_pubkey: ScriptBuf::default(), - }); - witness_utxo_to_clean.push(index); - continue; - } - if input.final_script_sig.is_some() - || input.final_script_witness.is_some() - || input.partial_sigs.is_empty() - { - continue; - } - inputs_to_finalize.push(index); - } - - for index in inputs_to_finalize { - match psbt.finalize_inp_mut(&secp, index) { - Ok(_) => log::info!("Finalizing input at: {}", index), - Err(e) => log::warn!("Failed to finalize input at: {} | {}", index, e), - } - } - - for index in witness_utxo_to_clean { - psbt.inputs[index].witness_utxo = None; - } -} - -fn proposal_dummy_checks_bypass(proposal: &UncheckedProposal) -> WantsOutputs { - let proposal = proposal - .clone() - .check_broadcast_suitability(None, |_| Ok(true)) - .expect("Failed to check broadcast suitability"); - let proposal = proposal - .check_inputs_not_owned(|_input| Ok(false)) - .expect("Failed to check inputs not owned"); - let proposal = proposal - .check_no_inputs_seen_before(|_| Ok(false)) - .expect("Failed to check no inputs seen before"); - let proposal = proposal - .identify_receiver_outputs(|_| Ok(true)) - .expect("Failed to identify receiver outputs"); - proposal -} - -fn process_proposal_psbt( - proposal: &UncheckedProposal, - db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], - secp: &secp256k1::Secp256k1, -) -> Result { - let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); - if let Some((_, coin)) = coins.iter().next() { - // descs must always have 2 descriptors - assert_eq!(descs.len(), 2); - - let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); - let script_pubkey = receiver_derived_desc.script_pubkey(); - - let txs = db_conn.list_wallet_transactions(&[coin.outpoint.txid]); - let (db_tx, _, _) = txs.first().unwrap(); - - let tx = db_tx.clone(); - - let txin = TxIn { - previous_output: coin.outpoint, - // TODO(arturgontijo): Avoiding Validation(ValidationError(Proposal(MixedSequence))) - sequence: Sequence::from_hex("0xFFFFFFFD").unwrap(), - ..Default::default() - }; - - let txout = TxOut { - value: coin.amount, - script_pubkey, - }; - - let mut psbtin = Input { - non_witness_utxo: Some(tx.clone()), - witness_utxo: Some(txout.clone()), - ..Default::default() - }; - - receiver_derived_desc.update_psbt_in(&mut psbtin); - - let proposal = proposal_dummy_checks_bypass(&proposal); - let proposal = proposal.commit_outputs(); - let proposal = proposal.commit_inputs(); - - let proposal = proposal - .finalize_proposal( - |psbt| Ok(psbt.clone()), - None, - Some(bitcoin::FeeRate::from_sat_per_vb(150).unwrap()), - ) - .expect("Failed to finalize proposal"); - - let mut psbt = proposal.psbt().clone(); - - psbt.inputs.push(psbtin); - psbt.unsigned_tx.input.push(txin); - - psbt.outputs.push(Output::default()); - psbt.unsigned_tx.output.push(txout); - - return Ok(psbt); - } - - Err(()) -} - -pub fn payjoin_receiver_check( - db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], - secp: &secp256k1::Secp256k1, -) { - let mut payjoin_receivers = db_conn.get_all_payjoin_receivers(); - for (address, status, receiver, psbt) in payjoin_receivers.iter_mut() { - match status { - PayjoinReceiverStatus::Pending => { - log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); - let (req, ctx) = receiver - .extract_req(OHTTP_RELAY) - .expect("Failed to extract request"); - match post_request(req.clone()) { - Ok(resp) => { - let proposal = receiver - .process_res( - resp.bytes().expect("Failed to read response").as_ref(), - ctx, - ) - .expect("Failed to process response"); - if let Some(proposal) = proposal { - log::info!("[Payjoin] receiver got a proposal..."); - - let new_psbt = - process_proposal_psbt(&proposal, db_conn, descs, secp).unwrap(); - - // DB logic - log::info!("[Payjoin] Inserting PSBT and STATUS..."); - db_conn.update_payjoin_receiver_status( - &address, - PayjoinReceiverStatus::Signing, - new_psbt.to_string(), - ); - db_conn.store_spend(&new_psbt); - - log::info!("[Payjoin] PSBT in DB: {}", new_psbt.to_string()); - } else { - log::info!("empty proposal") - } - } - Err(err) => { - log::error!("[Payjoin] payjoin_receiver_check(): {} -> {}", req.url, err) - } - } - } - PayjoinReceiverStatus::Signing => { - log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); - let psbt = match Psbt::from_str(psbt) { - Ok(psbt) => psbt, - Err(err) => { - log::error!( - "[Payjoin] payjoin_receiver_check(psbt_from): len={} -> {}", - psbt.len(), - err - ); - continue; - } - }; - let txid = psbt.unsigned_tx.compute_txid(); - match db_conn.spend_tx(&txid) { - Some(psbt) => { - let mut is_signed = false; - for psbtin in &psbt.inputs { - if !psbtin.partial_sigs.is_empty() { - log::info!("PSBT was signed!"); - is_signed = true; - break; - } - } - - if is_signed { - let (req, ctx) = receiver - .extract_req( - Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"), - ) - .expect("Failed to extract request"); - match post_request(req.clone()) { - Ok(resp) => { - let proposal = receiver - .process_res( - resp.bytes().expect("Failed to read response").as_ref(), - ctx, - ) - .expect("Failed to process response"); - if let Some(proposal) = proposal { - log::info!("[Payjoin] receiver got a proposal..."); - - let mut psbt = psbt.clone(); - finalize_psbt(&mut psbt, secp); - - let proposal = proposal_dummy_checks_bypass(&proposal); - let proposal = proposal.commit_outputs(); - let proposal = proposal.commit_inputs(); - - let mut proposal = proposal - .finalize_proposal( - |_| Ok(psbt.clone()), - None, - Some( - bitcoin::FeeRate::from_sat_per_vb(150).unwrap(), - ), - ) - .expect("Failed to finalize proposal"); - - let (req, ctx) = proposal - .extract_req( - Url::from_str(OHTTP_RELAY) - .expect("Invalid OHTTP relay"), - ) - .expect("Failed to extract request"); - - // Respond to sender - log::info!("[Payjoin] receiver responding to sender..."); - match post_request(req.clone()) { - Ok(resp) => { - let _proposal = receiver - .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) - .expect("Failed to process response"); - // Update status of receiver - db_conn.update_payjoin_receiver_status( - &address, - PayjoinReceiverStatus::Completed, - psbt.to_string(), - ); - }, - Err(err) => log::error!( - "[Payjoin] payjoin_receiver_check(respond_to_sender): {} -> {}", - req.url, err - ), - } - } else { - log::info!("empty proposal") - } - } - Err(err) => log::error!( - "[Payjoin] payjoin_receiver_check(Signed): {} -> {}", - req.url, - err - ), - } - } - } - None => {} - } - } - _ => {} - } - } -} - -fn payjoin_sender_check(db_conn: &mut Box) { - let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); - let payjoin_senders = db_conn.get_all_payjoin_senders(); - for (bip21, txid, status, maybe_sender) in payjoin_senders { - match status { - PayjoinSenderStatus::Pending => { - log::info!("[Payjoin] PayjoinSenderStatus: {:?} | {}", status, bip21); - - let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); - - let pj_uri = Uri::try_from(bip21.as_str()) - .expect("Invalid BIP21") - .assume_checked() - .check_pj_supported() - .expect("Invalid PJ BIP21"); - - let new_sender = SenderBuilder::new(psbt, pj_uri) - .build_recommended(bitcoin::FeeRate::BROADCAST_MIN) - .expect("Failed to build sender"); - - // TODO: should just be able to load a sender from the db, and not use the NoopPersister. - let storage_token = new_sender - .persist(&mut NoopPersister) - .expect("Failed to persist sender"); - - let sender = - Sender::load(storage_token, &NoopPersister).expect("Failed to load sender"); - - let (post_req, _) = sender - .extract_v2(ohttp_url.clone()) - .expect("Failed to extract v2"); - // Send original PSBT to the receiver via the BIP77 directory - match post_request(post_req.clone()) { - Ok(_) => { - log::info!("[Payjoin] Updating PSBT's STATUS..."); - db_conn.update_payjoin_sender_status( - txid, - PayjoinSenderStatus::WaitingReceiver, - Some(sender), - ); - } - Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), - } - } - PayjoinSenderStatus::WaitingReceiver => { - log::info!( - "[Payjoin] PayjoinSenderStatus: {:?} | sender_is_set={}", - status, - maybe_sender.is_some() - ); - if let Some(sender) = maybe_sender { - let (post_req, post_ctx) = sender - .extract_v2(ohttp_url.clone()) - .expect("Failed to extract v2"); - - match post_request(post_req.clone()) { - Ok(resp) => { - let get_ctx = post_ctx - .process_response( - resp.bytes().expect("Must be valid response").as_ref(), - ) - .expect("Failed to process response"); - - // Read the response from the receiver via the BIP77 directory - let (get_req, ohttp_ctx) = get_ctx - .extract_req(OHTTP_RELAY) - .expect("Failed to extract get request"); - - let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); - - // TODO(arturgontijo): PDK removes these fields but we need them so GUI can properly sign the inputs - let mut input_fields_to_restore = vec![]; - for (index, input) in psbt.inputs.iter().enumerate() { - input_fields_to_restore.push(( - index, - input.witness_script.clone(), - input.bip32_derivation.clone(), - )); - } - - match post_request(get_req.clone()) { - Ok(resp) => { - log::info!("Payjoin sender got a final PSBT..."); - - let mut psbt = match get_ctx.process_response( - resp.bytes().expect("Failed to read response").as_ref(), - ohttp_ctx, - ) { - Ok(Some(psbt)) => psbt, - Ok(None) => { - // nothing to do yet, no response - log::warn!("Nothing to do yet, no response..."); - continue; - } - Err(e) => { - log::warn!( - "Failed to process payjoin sender response: {:?}", - e - ); - // TODO: handle error - continue; - } - }; - - // TODO(arturgontijo): Restoring witness_scripts and bip32_bip32_derivation so GUI can sign them - for (index, witness_script, bip32_derivation) in - input_fields_to_restore - { - psbt.inputs[index].witness_script = witness_script; - psbt.inputs[index].bip32_derivation = bip32_derivation; - } - - // Store updated Payjoin psbt - log::info!( - "Updated Payjoin psbt: {} -> {}", - txid, - psbt.unsigned_tx.compute_txid() - ); - db_conn.store_spend(&psbt); - - log::info!("Deleting original Payjoin psbt (txid={})", txid); - db_conn.delete_spend(&txid); - - // Mark the sender as completed - db_conn.update_payjoin_sender_status( - txid, - PayjoinSenderStatus::Completed, - None, - ); - } - Err(e) => log::warn!("Failed to get receiver's proposal: {:?}", e), - } - } - Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), - } - } - } - _ => {} - } - } -} - /// Update our state from the Bitcoin backend. pub fn poll( bit: &mut sync::Arc>, From cb6cb4b2565efcb0bed5b84b91e44d58ea00f2b9 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Wed, 21 May 2025 07:57:13 +0400 Subject: [PATCH 05/72] [PDK] Use try_preserving_privacy() --- liana-gui/src/app/state/psbt.rs | 7 +- liana-gui/src/app/state/spend/step.rs | 18 +- liana-gui/src/daemon/client/mod.rs | 5 + liana-gui/src/daemon/embedded.rs | 10 ++ liana-gui/src/daemon/mod.rs | 10 +- liana-gui/src/daemon/model.rs | 32 +++- .../services/connect/client/backend/mod.rs | 5 + liana/src/signer.rs | 13 +- lianad/src/bitcoin/mod.rs | 1 - lianad/src/bitcoin/payjoin/sender.rs | 157 ----------------- lianad/src/bitcoin/poller/looper.rs | 6 +- lianad/src/commands/mod.rs | 38 +++- lianad/src/database/mod.rs | 36 ++-- lianad/src/database/sqlite/mod.rs | 102 +++++------ lianad/src/database/sqlite/schema.rs | 3 +- lianad/src/lib.rs | 1 + lianad/src/{bitcoin => }/payjoin/helpers.rs | 0 lianad/src/{bitcoin => }/payjoin/mod.rs | 1 + lianad/src/{bitcoin => }/payjoin/receiver.rs | 112 ++++++------ lianad/src/payjoin/sender.rs | 166 ++++++++++++++++++ lianad/src/payjoin/types.rs | 48 +++++ lianad/src/testutils.rs | 23 +-- 22 files changed, 481 insertions(+), 313 deletions(-) delete mode 100644 lianad/src/bitcoin/payjoin/sender.rs rename lianad/src/{bitcoin => }/payjoin/helpers.rs (100%) rename lianad/src/{bitcoin => }/payjoin/mod.rs (75%) rename lianad/src/{bitcoin => }/payjoin/receiver.rs (76%) create mode 100644 lianad/src/payjoin/sender.rs create mode 100644 lianad/src/payjoin/types.rs diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index aaf9462c1..b2fed86d9 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -187,13 +187,12 @@ impl PsbtState { Message::View(view::Message::Spend(view::SpendTxMessage::PayjoinInitiated)) => { self.tx.status = SpendStatus::PayjoinInitiated; self.modal = None; - if let Some(bip21) = self.tx.bip21.clone() { - // TODO: remove clone + if let Some(payjoin_info) = self.tx.payjoin_info.clone() { let psbt = self.tx.psbt.clone(); return Task::perform( async move { daemon - .send_payjoin(bip21.clone(), &psbt) + .send_payjoin(payjoin_info.bip21, &psbt) .await .map_err(|e| e.into()) }, @@ -560,7 +559,7 @@ impl Modal for SignModal { // TODO(arturgontijo): Use better design. Maybe checking for foreign inputs. // Payjoin Receiver Side let psbt_ready = psbt.clone().extract_tx(); - if tx.bip21.is_none() && psbt_ready.is_err() { + if tx.payjoin_info.is_none() && psbt_ready.is_err() { tx.status = SpendStatus::PayjoinProposalReady; } diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index e2d62cc5d..c7167e49f 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -18,7 +18,10 @@ use liana::{ }, spend::{SpendCreationError, MAX_FEERATE}, }; -use lianad::commands::ListCoinsEntry; +use lianad::{ + commands::ListCoinsEntry, + payjoin::types::{PayjoinInfo, PayjoinReceiverStatus, PayjoinSenderStatus}, +}; use liana_ui::{component::form, widget::Element}; use payjoin::Uri; @@ -966,7 +969,18 @@ impl SaveSpend { impl Step for SaveSpend { fn load(&mut self, _coins: &[Coin], _tip_height: i32, draft: &TransactionDraft) { let (psbt, warnings) = draft.generated.clone().unwrap(); + let bip21 = draft.recipients.get(0).unwrap().bip21.value.clone(); + let payjoin_info = if bip21.is_empty() { + None + } else { + Some(PayjoinInfo { + bip21, + sender_status: Some(PayjoinSenderStatus::Pending), + receiver_status: Some(PayjoinReceiverStatus::Pending), + }) + }; + let mut tx = SpendTx::new( None, psbt, @@ -974,7 +988,7 @@ impl Step for SaveSpend { &self.wallet.main_descriptor, &self.curve, draft.network, - Some(bip21), + payjoin_info, ); tx.labels.clone_from(&draft.labels); diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 49948f30c..343b5b1c5 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,6 +5,7 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; +use lianad::payjoin::types::PayjoinInfo; use payjoin::{OhttpKeys, Url}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -100,6 +101,10 @@ impl Daemon for Lianad { unimplemented!() } + async fn get_sender_payjoin(&self, _txid: &Txid) -> Result, DaemonError> { + unimplemented!() + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 56eedbb16..fedaac231 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,5 +1,6 @@ use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; +use lianad::payjoin::types::PayjoinInfo; use payjoin::{OhttpKeys, Url}; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; @@ -122,6 +123,15 @@ impl Daemon for EmbeddedDaemon { .await } + async fn get_sender_payjoin(&self, txid: &Txid) -> Result, DaemonError> { + self.command(|daemon| { + daemon + .get_sender_payjoin(txid) + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 3a4a41a73..89231a159 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -15,6 +15,7 @@ use liana::miniscript::bitcoin::{ }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; +use lianad::payjoin::types::PayjoinInfo; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -111,6 +112,7 @@ pub trait Daemon: Debug { ohttp_keys: OhttpKeys, ) -> Result; async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; + async fn get_sender_payjoin(&self, txid: &Txid) -> Result, DaemonError>; async fn update_deriv_indexes( &self, receive: Option, @@ -205,6 +207,10 @@ pub trait Daemon: Debug { .cloned() .collect(); + let payjoin_info = self + .get_sender_payjoin(&tx.psbt.unsigned_tx.compute_txid()) + .await?; + spend_txs.push(model::SpendTx::new( tx.updated_at, tx.psbt, @@ -212,9 +218,7 @@ pub trait Daemon: Debug { &info.descriptors.main, &curve, info.network, - // TODO: BIP21 loaded from the backend will always be ignored - // because it is not part of the `ListSpendEntry` - None, + payjoin_info, )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 9d9ec7a0e..561635172 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -14,6 +14,7 @@ pub use lianad::commands::{ CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, }; +use lianad::payjoin::types::{PayjoinInfo, PayjoinSenderStatus}; pub type Coin = ListCoinsEntry; @@ -52,7 +53,7 @@ pub struct SpendTx { pub sigs: PartialSpendInfo, pub updated_at: Option, pub kind: TransactionKind, - pub bip21: Option, + pub payjoin_info: Option, } #[derive(PartialOrd, Ord, Debug, Clone, PartialEq, Eq)] @@ -73,7 +74,7 @@ impl SpendTx { desc: &LianaDescriptor, secp: &secp256k1::Secp256k1, network: Network, - bip21: Option, + payjoin_info: Option, ) -> Self { // Use primary path if no inputs are using a relative locktime. let use_primary_path = !psbt @@ -150,7 +151,11 @@ impl SpendTx { // One input coin is missing, the psbt is deprecated for now. if coins_map.len() != psbt.inputs.len() { // TODO(arturgontijo): Skip for now... - log::warn!("Not deprecating..."); + log::warn!( + "Not deprecating... coins_map.len({}) != psbt.inputs.len({})", + coins_map.len(), + psbt.inputs.len() + ); // status = SpendStatus::Deprecated } @@ -195,16 +200,35 @@ impl SpendTx { status, sigs, network, - bip21, + payjoin_info, } } /// Returns the path ready if it exists. pub fn path_ready(&self) -> Option<&PathSpendInfo> { let path = self.sigs.primary_path(); + + // TODO(arturgontijo): We should count the sigs, just in case. + if let Some(payjoin_info) = &self.payjoin_info { + match payjoin_info.sender_status { + Some(PayjoinSenderStatus::Completed) => { + let has_sigs = self + .psbt + .inputs + .iter() + .any(|psbtin| !psbtin.partial_sigs.is_empty()); + if has_sigs { + return Some(path); + } + } + _ => {} + } + }; + if path.sigs_count >= path.threshold { return Some(path); } + self.sigs .recovery_paths() .values() diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index 30c242189..1924f507e 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -15,6 +15,7 @@ use lianad::{ bip329::Labels, commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, + payjoin::types::PayjoinInfo, }; use payjoin::{OhttpKeys, Url}; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; @@ -624,6 +625,10 @@ impl Daemon for BackendWalletClient { unimplemented!() } + async fn get_sender_payjoin(&self, _txid: &Txid) -> Result, DaemonError> { + unimplemented!() + } + async fn update_deriv_indexes( &self, _receive: Option, diff --git a/liana/src/signer.rs b/liana/src/signer.rs index 8f5f940d7..a04edca3a 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -367,10 +367,15 @@ impl HotSigner { .iter() .filter_map(|psbt_in| psbt_in.witness_utxo.clone()) .collect(); - // TODO(arturgontijo): Skip for now... - // if prevouts.len() != psbt.inputs.len() { - // return Err(SignerError::IncompletePsbt); - // } + if prevouts.len() != psbt.inputs.len() { + // TODO(arturgontijo): Skip for now... + log::warn!( + "Not throwing SignerError::IncompletePsbt: prevouts.len({}) != psbt.inputs.len({})", + prevouts.len(), + psbt.inputs.len() + ); + // return Err(SignerError::IncompletePsbt); + } // Sign each input in the PSBT. for i in 0..psbt.inputs.len() { diff --git a/lianad/src/bitcoin/mod.rs b/lianad/src/bitcoin/mod.rs index c20e7006f..26e755df8 100644 --- a/lianad/src/bitcoin/mod.rs +++ b/lianad/src/bitcoin/mod.rs @@ -4,7 +4,6 @@ pub mod d; pub mod electrum; -pub mod payjoin; pub mod poller; use crate::bitcoin::d::{BitcoindError, CachedTxGetter, LSBlockEntry}; diff --git a/lianad/src/bitcoin/payjoin/sender.rs b/lianad/src/bitcoin/payjoin/sender.rs deleted file mode 100644 index 53c314ded..000000000 --- a/lianad/src/bitcoin/payjoin/sender.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::{ - bitcoin::payjoin::helpers::post_request, - database::{sqlite::PayjoinSenderStatus, DatabaseConnection}, -}; - -use std::{convert::TryFrom, str::FromStr}; - -use payjoin::{ - bitcoin::FeeRate, - persist::NoopPersister, - send::v2::{Sender, SenderBuilder}, - Uri, UriExt, Url, -}; - -use super::helpers::OHTTP_RELAY; - -pub fn payjoin_sender_check(db_conn: &mut Box) { - let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); - let payjoin_senders = db_conn.get_all_payjoin_senders(); - for (bip21, txid, status, maybe_sender) in payjoin_senders { - match status { - PayjoinSenderStatus::Pending => { - log::info!("[Payjoin] PayjoinSenderStatus: {:?} | {}", status, bip21); - - let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); - - let pj_uri = Uri::try_from(bip21.as_str()) - .expect("Invalid BIP21") - .assume_checked() - .check_pj_supported() - .expect("Invalid PJ BIP21"); - - let new_sender = SenderBuilder::new(psbt, pj_uri) - .build_recommended(FeeRate::BROADCAST_MIN) - .expect("Failed to build sender"); - - // TODO: should just be able to load a sender from the db, and not use the NoopPersister. - let storage_token = new_sender - .persist(&mut NoopPersister) - .expect("Failed to persist sender"); - - let sender = - Sender::load(storage_token, &NoopPersister).expect("Failed to load sender"); - - let (post_req, _) = sender - .extract_v2(ohttp_url.clone()) - .expect("Failed to extract v2"); - // Send original PSBT to the receiver via the BIP77 directory - match post_request(post_req.clone()) { - Ok(_) => { - log::info!("[Payjoin] Updating PSBT's STATUS..."); - db_conn.update_payjoin_sender_status( - txid, - PayjoinSenderStatus::WaitingReceiver, - Some(sender), - ); - } - Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), - } - } - PayjoinSenderStatus::WaitingReceiver => { - log::info!( - "[Payjoin] PayjoinSenderStatus: {:?} | sender_is_set={}", - status, - maybe_sender.is_some() - ); - if let Some(sender) = maybe_sender { - let (post_req, post_ctx) = sender - .extract_v2(ohttp_url.clone()) - .expect("Failed to extract v2"); - - match post_request(post_req.clone()) { - Ok(resp) => { - let get_ctx = post_ctx - .process_response( - resp.bytes().expect("Must be valid response").as_ref(), - ) - .expect("Failed to process response"); - - // Read the response from the receiver via the BIP77 directory - let (get_req, ohttp_ctx) = get_ctx - .extract_req(OHTTP_RELAY) - .expect("Failed to extract get request"); - - let psbt = db_conn.spend_tx(&txid).expect("Spend tx not found"); - - // TODO(arturgontijo): PDK removes these fields but we need them so GUI can properly sign the inputs - let mut input_fields_to_restore = vec![]; - for (index, input) in psbt.inputs.iter().enumerate() { - input_fields_to_restore.push(( - index, - input.witness_script.clone(), - input.bip32_derivation.clone(), - )); - } - - match post_request(get_req.clone()) { - Ok(resp) => { - log::info!("Payjoin sender got a final PSBT..."); - - let mut psbt = match get_ctx.process_response( - resp.bytes().expect("Failed to read response").as_ref(), - ohttp_ctx, - ) { - Ok(Some(psbt)) => psbt, - Ok(None) => { - // nothing to do yet, no response - log::warn!("Nothing to do yet, no response..."); - continue; - } - Err(e) => { - log::warn!( - "Failed to process payjoin sender response: {:?}", - e - ); - // TODO: handle error - continue; - } - }; - - // TODO(arturgontijo): Restoring witness_scripts and bip32_bip32_derivation so GUI can sign them - for (index, witness_script, bip32_derivation) in - input_fields_to_restore - { - psbt.inputs[index].witness_script = witness_script; - psbt.inputs[index].bip32_derivation = bip32_derivation; - } - - // Store updated Payjoin psbt - log::info!( - "Updated Payjoin psbt: {} -> {}", - txid, - psbt.unsigned_tx.compute_txid() - ); - db_conn.store_spend(&psbt); - - log::info!("Deleting original Payjoin psbt (txid={})", txid); - db_conn.delete_spend(&txid); - - // Mark the sender as completed - db_conn.update_payjoin_sender_status( - txid, - PayjoinSenderStatus::Completed, - None, - ); - } - Err(e) => log::warn!("Failed to get receiver's proposal: {:?}", e), - } - } - Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), - } - } - } - _ => {} - } - } -} diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index 7f8373e66..7941075a4 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -1,9 +1,7 @@ use crate::{ - bitcoin::{ - payjoin::{receiver::payjoin_receiver_check, sender::payjoin_sender_check}, - BitcoinInterface, BlockChainTip, UTxO, UTxOAddress, - }, + bitcoin::{BitcoinInterface, BlockChainTip, UTxO, UTxOAddress}, database::{Coin, DatabaseConnection, DatabaseInterface}, + payjoin::{receiver::payjoin_receiver_check, sender::payjoin_sender_check}, }; use std::{collections::HashSet, convert::TryInto, sync, thread, time}; diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 94e52ffa4..ed750619d 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -8,6 +8,7 @@ use crate::{ bitcoin::BitcoinInterface, database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, + payjoin::types::PayjoinInfo, poller::PollerMessage, DaemonControl, VERSION, }; @@ -43,7 +44,7 @@ use miniscript::{ }, psbt::PsbtExt, }; -use payjoin::{persist::NoopPersister, OhttpKeys, Url}; +use payjoin::{bitcoin::Txid, persist::NoopPersister, OhttpKeys, Url}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -405,6 +406,39 @@ impl DaemonControl { Ok(()) } + /// Get Payjoin URI (BIP21) and its sender/receiver status by txid + pub fn get_sender_payjoin(&self, txid: &Txid) -> Result, CommandError> { + let mut db_conn = self.db.connection(); + + let mut receiver_status = None; + for (_, db_txid, status, _, _) in db_conn.get_all_payjoin_receivers() { + if &db_txid == txid { + receiver_status = Some(status); + break; + } + } + + let mut bip21 = String::new(); + let mut sender_status = None; + for (db_bip21, db_txid, status, _) in db_conn.get_all_payjoin_senders() { + if &db_txid == txid { + sender_status = Some(status); + bip21 = db_bip21; + break; + } + } + + if receiver_status.is_some() || sender_status.is_some() { + Ok(Some(PayjoinInfo { + bip21, + sender_status, + receiver_status, + })) + } else { + Ok(None) + } + } + /// Update derivation indexes pub fn update_deriv_indexes( &self, @@ -854,7 +888,7 @@ impl DaemonControl { for index in 0..spend_psbt.inputs.len() { match spend_psbt.finalize_inp_mut(&self.secp, index) { Ok(_) => log::info!("Finalizing input at: {}", index), - Err(_) => log::info!("Failed to finalizing input at: {}", index), + Err(_) => log::warn!("Not finalizing input at: {}", index), } } diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 3c59d7978..a37838aab 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -10,6 +10,7 @@ use crate::{ schema::{DbBlockInfo, DbCoin, DbTip}, SqliteConn, SqliteDb, }, + payjoin::types::{PayjoinReceiverStatus, PayjoinSenderStatus}, }; use std::{ @@ -23,7 +24,6 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; use payjoin::{receive::v2::Receiver, send::v2::Sender}; -use sqlite::{PayjoinReceiverStatus, PayjoinSenderStatus}; /// Information about the wallet. /// @@ -209,17 +209,24 @@ pub trait DatabaseConnection { /// Get a all active payjoin receivers fn get_all_payjoin_receivers( &mut self, - ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)>; + ) -> Vec<( + bitcoin::Address, + bitcoin::Txid, + PayjoinReceiverStatus, + Receiver, + String, + )>; /// Update the status of a payjoin receiver fn update_payjoin_receiver_status( &mut self, address: &bitcoin::Address, + txid: bitcoin::Txid, status: PayjoinReceiverStatus, psbt_str: String, ); /// Create a payjoin sender - fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid); + fn create_payjoin_sender(&mut self, bip21: String, txid: bitcoin::Txid); /// Get a all active payjoin senders fn get_all_payjoin_senders( &mut self, @@ -227,9 +234,10 @@ pub trait DatabaseConnection { /// Update the status of a payjoin sender fn update_payjoin_sender_status( &mut self, - spend_tx_id: bitcoin::Txid, + txid: bitcoin::Txid, status: PayjoinSenderStatus, maybe_sender: Option, + maybe_new_txid: Option, ); // ------- @@ -467,21 +475,28 @@ impl DatabaseConnection for SqliteConn { fn get_all_payjoin_receivers( &mut self, - ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)> { + ) -> Vec<( + bitcoin::Address, + bitcoin::Txid, + PayjoinReceiverStatus, + Receiver, + String, + )> { self.get_all_payjoin_receivers() } fn update_payjoin_receiver_status( &mut self, address: &bitcoin::Address, + txid: bitcoin::Txid, status: PayjoinReceiverStatus, psbt_str: String, ) { - self.update_payjoin_receiver_status(address, status, psbt_str) + self.update_payjoin_receiver_status(address, txid, status, psbt_str) } - fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid) { - self.create_payjoin_sender(bip21, spend_tx_id) + fn create_payjoin_sender(&mut self, bip21: String, txid: bitcoin::Txid) { + self.create_payjoin_sender(bip21, txid) } fn get_all_payjoin_senders( @@ -494,9 +509,10 @@ impl DatabaseConnection for SqliteConn { &mut self, spend_tx_id: bitcoin::Txid, status: PayjoinSenderStatus, - sender: Option, + maybe_sender: Option, + maybe_new_txid: Option, ) { - self.update_payjoin_sender_status(spend_tx_id, status, sender) + self.update_payjoin_sender_status(spend_tx_id, status, maybe_sender, maybe_new_txid) } } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index ad05a5a44..30d2e8944 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -25,9 +25,10 @@ use crate::{ }, Coin, CoinStatus, LabelItem, }, + payjoin::types::{PayjoinReceiverStatus, PayjoinSenderStatus}, }; use liana::descriptors::LianaDescriptor; -use payjoin::{receive::v2::Receiver, send::v2::Sender}; +use payjoin::{bitcoin::Txid, receive::v2::Receiver, send::v2::Sender}; use std::{ cmp, @@ -215,48 +216,6 @@ impl SqliteDb { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PayjoinSenderStatus { - Pending = 0, - WaitingReceiver = 1, - Completed = 2, - // TODO: more specific enums for why it failed - Failed = 3, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PayjoinReceiverStatus { - Pending = 0, - Signing = 1, - Completed = 2, - // TODO: more specific enums for why it failed - Failed = 3, -} - -impl From for PayjoinSenderStatus { - fn from(status: i32) -> Self { - match status { - 0 => PayjoinSenderStatus::Pending, - 1 => PayjoinSenderStatus::WaitingReceiver, - 2 => PayjoinSenderStatus::Completed, - 3 => PayjoinSenderStatus::Failed, - _ => panic!("Invalid payjoin sender status: {}", status), - } - } -} - -impl From for PayjoinReceiverStatus { - fn from(status: i32) -> Self { - match status { - 0 => PayjoinReceiverStatus::Pending, - 1 => PayjoinReceiverStatus::Signing, - 2 => PayjoinReceiverStatus::Completed, - 3 => PayjoinReceiverStatus::Failed, - _ => panic!("Invalid payjoin receiver status: {}", status), - } - } -} - // We only support single wallet. The id of the wallet row is always 1. const WALLET_ID: i64 = 1; @@ -1034,13 +993,19 @@ impl SqliteConn { pub fn update_payjoin_receiver_status( &mut self, address: &bitcoin::Address, + txid: bitcoin::Txid, status: PayjoinReceiverStatus, psbt_str: String, ) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_receivers SET status = ?1, psbt = ?2 WHERE address = ?3", - rusqlite::params![status as i32, psbt_str, address.to_string()], + "UPDATE payjoin_receivers SET status = ?1, txid = ?2, psbt = ?3 WHERE address = ?4", + rusqlite::params![ + status as i32, + txid[..].to_vec(), + psbt_str, + address.to_string() + ], )?; Ok(()) }) @@ -1049,34 +1014,47 @@ impl SqliteConn { pub fn get_all_payjoin_receivers( &mut self, - ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)> { + ) -> Vec<( + bitcoin::Address, + bitcoin::Txid, + PayjoinReceiverStatus, + Receiver, + String, + )> { db_query( &mut self.conn, - "SELECT address, status, receiver, psbt FROM payjoin_receivers", + "SELECT address, txid, status, receiver, psbt FROM payjoin_receivers", rusqlite::params![], |row| { let address_str: String = row.get(0)?; let address = bitcoin::Address::from_str(&address_str) .unwrap() .assume_checked(); - let status: i32 = row.get(1)?; - let receiver_json: String = row.get(2)?; + let txid_str: String = match row.get(1) { + Ok(txid_str) => txid_str, + Err(_) => String::new(), + }; + let txid = match Txid::from_str(&txid_str) { + Ok(txid) => txid, + Err(_) => Txid::all_zeros(), + }; + let status: i32 = row.get(2)?; + let receiver_json: String = row.get(3)?; let receiver: Receiver = serde_json::from_str(&receiver_json).unwrap(); - let psbt_str: String = row.get(3)?; - Ok((address, status.into(), receiver, psbt_str)) + let psbt_str: String = row.get(4)?; + Ok((address, txid, status.into(), receiver, psbt_str)) }, ) .expect("Db must not fail") } /// Create a payjoin sender - pub fn create_payjoin_sender(&mut self, bip21: String, spend_tx_id: bitcoin::Txid) { + pub fn create_payjoin_sender(&mut self, bip21: String, txid: bitcoin::Txid) { let status = PayjoinSenderStatus::Pending; - let txid = spend_tx_id[..].to_vec(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_senders (bip21, spend_tx_id, status, sender) VALUES (?1, ?2, ?3, ?4)", - rusqlite::params![bip21, txid, status as i32, String::new()], + "INSERT INTO payjoin_senders (bip21, txid, status, sender) VALUES (?1, ?2, ?3, ?4)", + rusqlite::params![bip21, txid[..].to_vec(), status as i32, String::new()], )?; Ok(()) }) @@ -1088,7 +1066,7 @@ impl SqliteConn { ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { db_query( &mut self.conn, - "SELECT bip21, spend_tx_id, status, sender FROM payjoin_senders", + "SELECT bip21, txid, status, sender FROM payjoin_senders", rusqlite::params![], |row| { let bip21: String = row.get(0)?; @@ -1110,25 +1088,27 @@ impl SqliteConn { pub fn update_payjoin_sender_status( &mut self, - spend_tx_id: bitcoin::Txid, + txid: bitcoin::Txid, status: PayjoinSenderStatus, maybe_sender: Option, + maybe_new_txid: Option, ) { if let Some(sender) = maybe_sender { let sender_json = serde_json::to_string(&sender).unwrap(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_senders SET status = ?1, sender = ?2 WHERE spend_tx_id = ?3", - rusqlite::params![status as i32, sender_json, spend_tx_id[..].to_vec()], + "UPDATE payjoin_senders SET status = ?1, sender = ?2 WHERE txid = ?3", + rusqlite::params![status as i32, sender_json, txid[..].to_vec()], )?; Ok(()) }) .expect("Db must not fail"); } else { + let new_txid = maybe_new_txid.unwrap(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_senders SET status = ?1 WHERE spend_tx_id = ?2", - rusqlite::params![status as i32, spend_tx_id[..].to_vec()], + "UPDATE payjoin_senders SET status = ?1, sender = ?2, txid = ?3 WHERE txid = ?4", + rusqlite::params![status as i32, String::new(), new_txid[..].to_vec(), txid[..].to_vec()], )?; Ok(()) }) diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index e93fc5a30..3c78d560f 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -127,7 +127,7 @@ CREATE TABLE labels ( CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, bip21 TEXT NOT NULL, - spend_tx_id BLOB UNIQUE NOT NULL, + txid BLOB UNIQUE NOT NULL, status INTEGER NOT NULL CHECK (status IN (0,1,2,3)), sender TEXT NOT NULL ); @@ -136,6 +136,7 @@ CREATE TABLE payjoin_senders ( CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, address TEXT UNIQUE NOT NULL, + txid BLOB, status INTEGER NOT NULL CHECK (status IN (0,1,2,3)), receiver TEXT NOT NULL, psbt TEXT NOT NULL diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs index 8ff2306bc..3887a6531 100644 --- a/lianad/src/lib.rs +++ b/lianad/src/lib.rs @@ -4,6 +4,7 @@ pub mod config; mod database; pub mod datadir; mod jsonrpc; +pub mod payjoin; #[cfg(test)] mod testutils; diff --git a/lianad/src/bitcoin/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs similarity index 100% rename from lianad/src/bitcoin/payjoin/helpers.rs rename to lianad/src/payjoin/helpers.rs diff --git a/lianad/src/bitcoin/payjoin/mod.rs b/lianad/src/payjoin/mod.rs similarity index 75% rename from lianad/src/bitcoin/payjoin/mod.rs rename to lianad/src/payjoin/mod.rs index 782d782ad..c69b87879 100644 --- a/lianad/src/bitcoin/payjoin/mod.rs +++ b/lianad/src/payjoin/mod.rs @@ -1,3 +1,4 @@ mod helpers; pub mod receiver; pub mod sender; +pub mod types; diff --git a/lianad/src/bitcoin/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs similarity index 76% rename from lianad/src/bitcoin/payjoin/receiver.rs rename to lianad/src/payjoin/receiver.rs index 88357a47c..a054b0f08 100644 --- a/lianad/src/bitcoin/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -1,56 +1,49 @@ -use crate::{ - bitcoin::payjoin::helpers::{ - finalize_psbt, post_request, proposal_dummy_checks_bypass, OHTTP_RELAY, - }, - database::{sqlite::PayjoinReceiverStatus, CoinStatus, DatabaseConnection}, -}; - -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use liana::descriptors; use payjoin::{ - bitcoin::{ - psbt::{Input, Output}, - secp256k1, FeeRate, Psbt, Sequence, TxIn, - }, - receive::v2::UncheckedProposal, + bitcoin::{psbt::Input, secp256k1, FeeRate, OutPoint, Psbt, Sequence, TxIn}, + receive::{v2::UncheckedProposal, InputPair}, Url, }; +use crate::{ + database::{Coin, CoinStatus, DatabaseConnection}, + payjoin::helpers::{finalize_psbt, post_request, proposal_dummy_checks_bypass, OHTTP_RELAY}, +}; + +use super::types::PayjoinReceiverStatus; + pub fn process_proposal_psbt( proposal: &UncheckedProposal, db_conn: &mut Box, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) -> Result { + // descs must always have 2 descriptors + assert_eq!(descs.len(), 2); let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); - if let Some((_, coin)) = coins.iter().next() { - let proposal = proposal_dummy_checks_bypass(&proposal); - let proposal = proposal.commit_outputs(); - let proposal = proposal.commit_inputs(); - - let proposal = proposal - .finalize_proposal( - |psbt| Ok(psbt.clone()), - None, - Some(FeeRate::from_sat_per_vb(150).unwrap()), - ) - .expect("Failed to finalize proposal"); - - let mut psbt = proposal.psbt().clone(); - // descs must always have 2 descriptors - assert_eq!(descs.len(), 2); + let proposal = proposal_dummy_checks_bypass(&proposal); + let proposal = proposal.commit_outputs(); - let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); - - let txs = db_conn.list_wallet_transactions(&[coin.outpoint.txid]); + let mut candidate_inputs_map = HashMap::::new(); + for (outpoint, coin) in coins.iter() { + let txs = db_conn.list_wallet_transactions(&[outpoint.txid]); let (db_tx, _, _) = txs.first().unwrap(); let tx = db_tx.clone(); - let txout = tx.tx_out(coin.outpoint.vout as usize).unwrap().clone(); + let txout = tx.tx_out(outpoint.vout as usize).unwrap().clone(); + + let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); + + let txin = TxIn { + previous_output: outpoint.clone(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }; let mut psbtin = Input { non_witness_utxo: Some(tx.clone()), @@ -60,23 +53,40 @@ pub fn process_proposal_psbt( receiver_derived_desc.update_psbt_in(&mut psbtin); - let txin = TxIn { - previous_output: coin.outpoint, - // TODO(arturgontijo): Avoiding Validation(ValidationError(Proposal(MixedSequence))) - sequence: Sequence::from_hex("0xFFFFFFFD").unwrap(), - ..Default::default() - }; - - psbt.inputs.push(psbtin); - psbt.unsigned_tx.input.push(txin); - - psbt.outputs.push(Output::default()); - psbt.unsigned_tx.output.push(txout); + candidate_inputs_map.insert(outpoint.clone(), (*coin, txin, psbtin)); + } - return Ok(psbt); + let candidate_inputs = candidate_inputs_map + .values() + .map(|(_, txin, psbtin)| InputPair::new(txin.clone(), psbtin.clone()).unwrap()); + + let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); + + let proposal = proposal.contribute_inputs(vec![selected_input]).unwrap(); + let proposal = proposal.commit_inputs(); + + let proposal = proposal + .finalize_proposal( + |psbt| Ok(psbt.clone()), + None, + Some(FeeRate::from_sat_per_vb(150).unwrap()), + ) + .expect("Failed to finalize proposal"); + + let mut psbt = proposal.psbt().clone(); + + // TODO(arturgontijo): If we use a previous payjoin utxo it is breaking while broadcasting + for (index, psbtin) in psbt.inputs.iter_mut().enumerate() { + let outpoint = &psbt.unsigned_tx.input[index].previous_output; + if let Some((coin, txin, input)) = candidate_inputs_map.get(outpoint) { + *psbtin = input.clone(); + psbt.unsigned_tx.input[index] = txin.clone(); + let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); + receiver_derived_desc.update_psbt_in(psbtin); + } } - Err(()) + Ok(psbt) } pub fn payjoin_receiver_check( @@ -85,7 +95,7 @@ pub fn payjoin_receiver_check( secp: &secp256k1::Secp256k1, ) { let mut payjoin_receivers = db_conn.get_all_payjoin_receivers(); - for (address, status, receiver, psbt) in payjoin_receivers.iter_mut() { + for (address, _, status, receiver, psbt) in payjoin_receivers.iter_mut() { match status { PayjoinReceiverStatus::Pending => { log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); @@ -110,12 +120,13 @@ pub fn payjoin_receiver_check( log::info!("[Payjoin] Inserting PSBT and STATUS..."); db_conn.update_payjoin_receiver_status( &address, + new_psbt.unsigned_tx.compute_txid(), PayjoinReceiverStatus::Signing, new_psbt.to_string(), ); db_conn.store_spend(&new_psbt); - log::info!("[Payjoin] PSBT in DB: {}", new_psbt.to_string()); + log::info!("[Payjoin] Payjoin PSBT in DB!"); } else { log::info!("empty proposal") } @@ -199,6 +210,7 @@ pub fn payjoin_receiver_check( // Update status of receiver db_conn.update_payjoin_receiver_status( &address, + psbt.unsigned_tx.compute_txid(), PayjoinReceiverStatus::Completed, psbt.to_string(), ); @@ -212,7 +224,7 @@ pub fn payjoin_receiver_check( log::info!("empty proposal") } } - Err(err) => log::error!( + Err(err) => log::warn!( "[Payjoin] payjoin_receiver_check(Signed): {} -> {}", req.url, err diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs new file mode 100644 index 000000000..d784aa7b2 --- /dev/null +++ b/lianad/src/payjoin/sender.rs @@ -0,0 +1,166 @@ +use crate::database::DatabaseConnection; + +use crate::payjoin::{helpers::post_request, types::PayjoinSenderStatus}; + +use std::{collections::HashMap, convert::TryFrom, str::FromStr}; + +use payjoin::{ + bitcoin::{psbt::Input, FeeRate, OutPoint}, + persist::NoopPersister, + send::v2::{Sender, SenderBuilder}, + Uri, UriExt, Url, +}; + +use super::helpers::OHTTP_RELAY; + +pub fn payjoin_sender_check(db_conn: &mut Box) { + let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); + let payjoin_senders = db_conn.get_all_payjoin_senders(); + for (bip21, txid, status, maybe_sender) in payjoin_senders { + match status { + PayjoinSenderStatus::Pending => { + log::info!("[Payjoin] PayjoinSenderStatus: {:?} | {}", status, bip21); + + if let Some(psbt) = db_conn.spend_tx(&txid) { + let pj_uri = Uri::try_from(bip21.as_str()) + .expect("Invalid BIP21") + .assume_checked() + .check_pj_supported() + .expect("Invalid PJ BIP21"); + + let new_sender = SenderBuilder::new(psbt, pj_uri) + .build_recommended(FeeRate::BROADCAST_MIN) + .expect("Failed to build sender"); + + // TODO: should just be able to load a sender from the db, and not use the NoopPersister. + let storage_token = new_sender + .persist(&mut NoopPersister) + .expect("Failed to persist sender"); + + let sender = + Sender::load(storage_token, &NoopPersister).expect("Failed to load sender"); + + let (post_req, _) = sender + .extract_v2(ohttp_url.clone()) + .expect("Failed to extract v2"); + // Send original PSBT to the receiver via the BIP77 directory + match post_request(post_req.clone()) { + Ok(_) => { + log::info!("[Payjoin] Updating PSBT's STATUS..."); + db_conn.update_payjoin_sender_status( + txid, + PayjoinSenderStatus::WaitingReceiver, + Some(sender), + None, + ); + } + Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), + } + } + } + PayjoinSenderStatus::WaitingReceiver => { + log::info!( + "[Payjoin] PayjoinSenderStatus: {:?} | sender_is_set={}", + status, + maybe_sender.is_some() + ); + if let Some(sender) = maybe_sender { + let (post_req, post_ctx) = sender + .extract_v2(ohttp_url.clone()) + .expect("Failed to extract v2"); + + match post_request(post_req.clone()) { + Ok(resp) => { + let get_ctx = post_ctx + .process_response( + resp.bytes().expect("Must be valid response").as_ref(), + ) + .expect("Failed to process response"); + + // Read the response from the receiver via the BIP77 directory + let (get_req, ohttp_ctx) = get_ctx + .extract_req(OHTTP_RELAY) + .expect("Failed to extract get request"); + + if let Some(psbt) = db_conn.spend_tx(&txid) { + // TODO(arturgontijo): PDK removes fields that we need in the GUI to properly sign the inputs + let mut input_fields_to_restore = HashMap::::new(); + for (index, txin) in psbt.unsigned_tx.input.iter().enumerate() { + let mut input_without_sigs = psbt.inputs[index].clone(); + input_without_sigs.partial_sigs = Default::default(); + input_fields_to_restore + .insert(txin.previous_output.clone(), input_without_sigs); + } + + match post_request(get_req.clone()) { + Ok(resp) => { + log::info!("Payjoin sender got a final PSBT..."); + + let mut psbt = match get_ctx.process_response( + resp.bytes().expect("Failed to read response").as_ref(), + ohttp_ctx, + ) { + Ok(Some(psbt)) => psbt, + Ok(None) => { + // nothing to do yet, no response + log::warn!("Nothing to do yet, no response..."); + continue; + } + Err(e) => { + log::warn!( + "Failed to process payjoin sender response: {:?}", + e + ); + // TODO: handle error + continue; + } + }; + + // TODO(arturgontijo): Restoring witness_scripts and bip32_derivation so GUI can sign them + for (index, psbtin) in psbt.inputs.iter_mut().enumerate() { + let outpoint = + &psbt.unsigned_tx.input[index].previous_output; + if let Some(input) = + input_fields_to_restore.get(outpoint) + { + *psbtin = input.clone(); + } + } + + // Store updated Payjoin psbt + let new_txid = psbt.unsigned_tx.compute_txid(); + log::info!( + "Updated Payjoin psbt: {} -> {}", + txid, + new_txid, + ); + db_conn.store_spend(&psbt); + + log::info!( + "Deleting original Payjoin psbt (txid={})", + txid + ); + db_conn.delete_spend(&txid); + + // Mark the sender as completed + db_conn.update_payjoin_sender_status( + txid, + PayjoinSenderStatus::Completed, + None, + Some(new_txid), + ); + } + Err(e) => { + log::warn!("Failed to get receiver's proposal: {:?}", e) + } + } + } + } + Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), + } + } + } + _ => {} + } + } +} diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs new file mode 100644 index 000000000..c5d6840bf --- /dev/null +++ b/lianad/src/payjoin/types.rs @@ -0,0 +1,48 @@ +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PayjoinSenderStatus { + Pending = 0, + WaitingReceiver = 1, + Completed = 2, + // TODO: more specific enums for why it failed + Failed = 3, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PayjoinReceiverStatus { + Pending = 0, + Signing = 1, + Completed = 2, + // TODO: more specific enums for why it failed + Failed = 3, +} + +impl From for PayjoinSenderStatus { + fn from(status: i32) -> Self { + match status { + 0 => PayjoinSenderStatus::Pending, + 1 => PayjoinSenderStatus::WaitingReceiver, + 2 => PayjoinSenderStatus::Completed, + 3 => PayjoinSenderStatus::Failed, + _ => panic!("Invalid payjoin sender status: {}", status), + } + } +} + +impl From for PayjoinReceiverStatus { + fn from(status: i32) -> Self { + match status { + 0 => PayjoinReceiverStatus::Pending, + 1 => PayjoinReceiverStatus::Signing, + 2 => PayjoinReceiverStatus::Completed, + 3 => PayjoinReceiverStatus::Failed, + _ => panic!("Invalid payjoin receiver status: {}", status), + } + } +} + +#[derive(Debug, Clone)] +pub struct PayjoinInfo { + pub sender_status: Option, + pub receiver_status: Option, + pub bip21: String, +} diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 8d6dc6a00..08bc34ff0 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -2,10 +2,10 @@ use crate::{ bitcoin::{BitcoinInterface, Block, BlockChainTip, MempoolEntry, SyncProgress, UTxO}, config::{BitcoinConfig, Config}, database::{ - sqlite::PayjoinReceiverStatus, BlockInfo, Coin, CoinStatus, DatabaseConnection, - DatabaseInterface, LabelItem, Wallet, + BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet, }, datadir::DataDirectory, + payjoin::types::{PayjoinReceiverStatus, PayjoinSenderStatus}, DaemonControl, DaemonHandle, }; use liana::descriptors; @@ -542,13 +542,20 @@ impl DatabaseConnection for DummyDatabase { fn get_all_payjoin_receivers( &mut self, - ) -> Vec<(bitcoin::Address, PayjoinReceiverStatus, Receiver, String)> { + ) -> Vec<( + bitcoin::Address, + bitcoin::Txid, + PayjoinReceiverStatus, + Receiver, + String, + )> { todo!() } fn update_payjoin_receiver_status( &mut self, _address: &bitcoin::Address, + _txid: bitcoin::Txid, _status: PayjoinReceiverStatus, _psbt_str: String, ) { @@ -561,20 +568,16 @@ impl DatabaseConnection for DummyDatabase { fn get_all_payjoin_senders( &mut self, - ) -> Vec<( - String, - bitcoin::Txid, - crate::database::sqlite::PayjoinSenderStatus, - Option, - )> { + ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { todo!() } fn update_payjoin_sender_status( &mut self, _spend_tx_id: bitcoin::Txid, - _status: crate::database::sqlite::PayjoinSenderStatus, + _status: PayjoinSenderStatus, _maybe_sender: Option, + _maybe_new_txid: Option, ) { todo!() } From 40c0500fb32f5f494cf80e21c015477e9453e537 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Wed, 28 May 2025 18:48:50 -0300 Subject: [PATCH 06/72] [PDK] Use SLE --- Cargo.lock | 1 - liana-gui/Cargo.toml | 3 +- liana-gui/src/app/state/psbt.rs | 2 +- liana-gui/src/app/state/spend/step.rs | 6 +- liana-gui/src/daemon/model.rs | 4 +- liana/src/signer.rs | 1 + lianad/Cargo.toml | 3 +- lianad/src/bitcoin/poller/looper.rs | 4 +- lianad/src/commands/mod.rs | 79 ++++-- lianad/src/database/mod.rs | 134 ++++++---- lianad/src/database/sqlite/mod.rs | 206 ++++++++------- lianad/src/database/sqlite/schema.rs | 13 +- lianad/src/payjoin/db.rs | 183 +++++++++++++ lianad/src/payjoin/helpers.rs | 25 +- lianad/src/payjoin/mod.rs | 1 + lianad/src/payjoin/receiver.rs | 361 ++++++++++++++++---------- lianad/src/payjoin/sender.rs | 276 +++++++++----------- lianad/src/payjoin/types.rs | 31 ++- lianad/src/testutils.rs | 67 +++-- 19 files changed, 872 insertions(+), 528 deletions(-) create mode 100644 lianad/src/payjoin/db.rs diff --git a/Cargo.lock b/Cargo.lock index 0b7e8905e..20f0f6184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,6 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.23.0" -source = "git+https://github.com/arturgontijo/rust-payjoin.git?branch=liana-poc#05ca2e8e4606315321634d4af64207253ecb0d1c" dependencies = [ "bhttp", "bitcoin", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 159a2462d..f1144da26 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,7 +55,8 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} +# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} +payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"]} [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index b2fed86d9..8e65bd493 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -559,7 +559,7 @@ impl Modal for SignModal { // TODO(arturgontijo): Use better design. Maybe checking for foreign inputs. // Payjoin Receiver Side let psbt_ready = psbt.clone().extract_tx(); - if tx.payjoin_info.is_none() && psbt_ready.is_err() { + if psbt_ready.is_err() { tx.status = SpendStatus::PayjoinProposalReady; } diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index c7167e49f..d68434dcb 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -20,7 +20,7 @@ use liana::{ }; use lianad::{ commands::ListCoinsEntry, - payjoin::types::{PayjoinInfo, PayjoinReceiverStatus, PayjoinSenderStatus}, + payjoin::types::{PayjoinInfo, PayjoinStatus}, }; use liana_ui::{component::form, widget::Element}; @@ -976,8 +976,8 @@ impl Step for SaveSpend { } else { Some(PayjoinInfo { bip21, - sender_status: Some(PayjoinSenderStatus::Pending), - receiver_status: Some(PayjoinReceiverStatus::Pending), + sender_status: Some(PayjoinStatus::Pending), + receiver_status: Some(PayjoinStatus::Pending), }) }; diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 561635172..c26999fbd 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -14,7 +14,7 @@ pub use lianad::commands::{ CreateSpendResult, GetAddressResult, GetInfoResult, GetLabelsResult, LabelItem, ListCoinsEntry, ListCoinsResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, }; -use lianad::payjoin::types::{PayjoinInfo, PayjoinSenderStatus}; +use lianad::payjoin::types::{PayjoinInfo, PayjoinStatus}; pub type Coin = ListCoinsEntry; @@ -211,7 +211,7 @@ impl SpendTx { // TODO(arturgontijo): We should count the sigs, just in case. if let Some(payjoin_info) = &self.payjoin_info { match payjoin_info.sender_status { - Some(PayjoinSenderStatus::Completed) => { + Some(PayjoinStatus::Completed) => { let has_sigs = self .psbt .inputs diff --git a/liana/src/signer.rs b/liana/src/signer.rs index a04edca3a..4f39e9d1c 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -362,6 +362,7 @@ impl HotSigner { let master_fingerprint = self.fingerprint(secp); let mut sighash_cache = sighash::SighashCache::new(&psbt.unsigned_tx); + // TODO(arturgontijo): my_prevouts (my psbt.inputs only) let prevouts: Vec<_> = psbt .inputs .iter() diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 188fdda29..fde9a1c65 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,5 +60,6 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} +# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} +payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"]} reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index 7941075a4..de863d0fd 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -406,8 +406,8 @@ pub fn poll( let mut db_conn = db.connection(); updates(&mut db_conn, bit, descs, secp); rescan_check(&mut db_conn, bit, descs, secp); - payjoin_sender_check(&mut db_conn); - payjoin_receiver_check(&mut db_conn, descs, secp); + payjoin_sender_check(db); + payjoin_receiver_check(db, descs, secp); let now: u32 = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .expect("current system time must be later than epoch") diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index ed750619d..2e7093582 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -8,7 +8,10 @@ use crate::{ bitcoin::BitcoinInterface, database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, - payjoin::types::PayjoinInfo, + payjoin::{ + db::{ReceiverPersister, SenderPersister}, + types::PayjoinInfo, + }, poller::PollerMessage, DaemonControl, VERSION, }; @@ -30,9 +33,9 @@ use utils::{ use std::{ collections::{hash_map, HashMap, HashSet}, - convert::TryInto, + convert::{TryFrom, TryInto}, fmt, - sync::{self, mpsc}, + sync::{self, mpsc, Arc}, time::SystemTime, }; @@ -44,7 +47,12 @@ use miniscript::{ }, psbt::PsbtExt, }; -use payjoin::{bitcoin::Txid, persist::NoopPersister, OhttpKeys, Url}; +use payjoin::{ + bitcoin::{FeeRate, Txid}, + receive::v2::{Receiver, UninitializedReceiver}, + send::v2::SenderBuilder, + OhttpKeys, Uri, UriExt, Url, +}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -378,31 +386,50 @@ impl DaemonControl { .derive(new_index, &self.secp) .address(self.config.bitcoin_config.network); - let pj_receiver = payjoin::receive::v2::NewReceiver::new( + let persister = ReceiverPersister::new(Arc::new(self.db.clone())).unwrap(); + let session = Receiver::::create_session( address.clone(), directory.clone(), ohttp_keys.clone(), None, ) + .save(&persister) .unwrap(); - let storage_token = pj_receiver.persist(&mut NoopPersister).unwrap(); - let receiver = - payjoin::receive::v2::Receiver::load(storage_token, &mut NoopPersister).unwrap(); - - let mut payjoin_uri = receiver.pj_uri(); + let mut payjoin_uri = session.pj_uri(); // HACK: hardcoded amount for now payjoin_uri.amount = Some(bitcoin::Amount::from_sat(10_000)); - db_conn.create_payjoin_receiver(&address, receiver.clone(), "".to_string()); GetAddressResult::new(address, new_index, payjoin_uri.to_string()) } - /// Initate a payjoin sender + /// Initiate a payjoin sender pub fn init_payjoin_sender(&self, bip21: String, psbt: &Psbt) -> Result<(), CommandError> { - let mut db_conn = self.db.connection(); - let txid = psbt.clone().extract_tx().unwrap().compute_txid(); - db_conn.create_payjoin_sender(bip21, txid); + let uri = Uri::try_from(bip21.clone()) + .map_err(|e| format!("Failed to create URI from BIP21: {}", e)) + .unwrap(); + let uri = uri.assume_checked(); + let uri = uri + .check_pj_supported() + .map_err(|_| format!("URI does not support Payjoin")) + .unwrap(); + + let persister = SenderPersister::new(Arc::new(self.db.clone())).unwrap(); + let _sender = SenderBuilder::new(psbt.clone(), uri) + .build_recommended(FeeRate::BROADCAST_MIN) + .save(&persister) + .unwrap(); + + let session_id = persister.session_id; + if let Some(mut session) = self.db.connection().payjoin_get_sender_session(&session_id) { + session.bip21 = Some(bip21); + session.psbt = Some(psbt.clone()); + session.txid = Some(psbt.unsigned_tx.compute_txid()); + self.db + .connection() + .update_payjoin_sender_status(&session_id, session); + } + Ok(()) } @@ -411,20 +438,24 @@ impl DaemonControl { let mut db_conn = self.db.connection(); let mut receiver_status = None; - for (_, db_txid, status, _, _) in db_conn.get_all_payjoin_receivers() { - if &db_txid == txid { - receiver_status = Some(status); - break; + for (_, session) in db_conn.payjoin_get_all_receiver_sessions() { + if let Some(db_txid) = session.txid { + if &db_txid == txid { + receiver_status = Some(session.status); + break; + } } } let mut bip21 = String::new(); let mut sender_status = None; - for (db_bip21, db_txid, status, _) in db_conn.get_all_payjoin_senders() { - if &db_txid == txid { - sender_status = Some(status); - bip21 = db_bip21; - break; + for (_, session) in db_conn.payjoin_get_all_sender_sessions() { + if let Some(db_txid) = session.txid { + if &db_txid == txid { + sender_status = Some(session.status); + bip21 = session.bip21.unwrap_or_default(); + break; + } } } diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index a37838aab..854b96484 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -10,7 +10,7 @@ use crate::{ schema::{DbBlockInfo, DbCoin, DbTip}, SqliteConn, SqliteDb, }, - payjoin::types::{PayjoinReceiverStatus, PayjoinSenderStatus}, + payjoin::db::{SessionId, SessionWrapper}, }; use std::{ @@ -23,7 +23,7 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; -use payjoin::{receive::v2::Receiver, send::v2::Sender}; +use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent}; /// Information about the wallet. /// @@ -199,45 +199,56 @@ pub trait DatabaseConnection { /// Payjoin - /// Create a payjoin receiver - fn create_payjoin_receiver( + /// Get the next Session Id + fn payjoin_next_id(&mut self) -> u64; + + /// Save Receiver Session + fn payjoin_save_receiver_session( &mut self, - address: &bitcoin::Address, - receiver: Receiver, - psbt_str: String, + session_id: &SessionId, + session: SessionWrapper, ); - /// Get a all active payjoin receivers - fn get_all_payjoin_receivers( + + /// Get a Receiver Session by Id + fn payjoin_get_receiver_session( &mut self, - ) -> Vec<( - bitcoin::Address, - bitcoin::Txid, - PayjoinReceiverStatus, - Receiver, - String, - )>; + session_id: &SessionId, + ) -> Option>; + + /// Get all Receiver Sessions + fn payjoin_get_all_receiver_sessions( + &mut self, + ) -> Vec<(SessionId, SessionWrapper)>; + /// Update the status of a payjoin receiver fn update_payjoin_receiver_status( &mut self, - address: &bitcoin::Address, - txid: bitcoin::Txid, - status: PayjoinReceiverStatus, - psbt_str: String, + session_id: &SessionId, + session: SessionWrapper, ); /// Create a payjoin sender - fn create_payjoin_sender(&mut self, bip21: String, txid: bitcoin::Txid); + fn payjoin_save_sender_session( + &mut self, + session_id: &SessionId, + session: SessionWrapper, + ); + + fn payjoin_get_sender_session( + &mut self, + session_id: &SessionId, + ) -> Option>; + /// Get a all active payjoin senders - fn get_all_payjoin_senders( + fn payjoin_get_all_sender_sessions( &mut self, - ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)>; + ) -> Vec<(SessionId, SessionWrapper)>; + /// Update the status of a payjoin sender fn update_payjoin_sender_status( &mut self, - txid: bitcoin::Txid, - status: PayjoinSenderStatus, - maybe_sender: Option, - maybe_new_txid: Option, + session_id: &SessionId, + session: SessionWrapper, ); // ------- @@ -464,55 +475,66 @@ impl DatabaseConnection for SqliteConn { .collect() } - fn create_payjoin_receiver( + fn payjoin_next_id(&mut self) -> u64 { + self.payjoin_next_id() + } + + fn payjoin_save_receiver_session( &mut self, - address: &bitcoin::Address, - receiver: Receiver, - psbt_str: String, + session_id: &SessionId, + session: SessionWrapper, ) { - self.create_payjoin_receiver(address, receiver, psbt_str) + self.payjoin_save_receiver_session(session_id, session) + } + + fn payjoin_get_receiver_session( + &mut self, + session_id: &SessionId, + ) -> Option> { + self.payjoin_get_receiver_session(session_id) } - fn get_all_payjoin_receivers( + fn payjoin_get_all_receiver_sessions( &mut self, - ) -> Vec<( - bitcoin::Address, - bitcoin::Txid, - PayjoinReceiverStatus, - Receiver, - String, - )> { - self.get_all_payjoin_receivers() + ) -> Vec<(SessionId, SessionWrapper)> { + self.payjoin_get_all_receiver_sessions() } fn update_payjoin_receiver_status( &mut self, - address: &bitcoin::Address, - txid: bitcoin::Txid, - status: PayjoinReceiverStatus, - psbt_str: String, + session_id: &SessionId, + session: SessionWrapper, + ) { + self.update_payjoin_receiver_status(session_id, session) + } + + fn payjoin_save_sender_session( + &mut self, + session_id: &SessionId, + session: SessionWrapper, ) { - self.update_payjoin_receiver_status(address, txid, status, psbt_str) + self.payjoin_save_sender_session(session_id, session) } - fn create_payjoin_sender(&mut self, bip21: String, txid: bitcoin::Txid) { - self.create_payjoin_sender(bip21, txid) + fn payjoin_get_sender_session( + &mut self, + session_id: &SessionId, + ) -> Option> { + self.payjoin_get_sender_session(session_id) } - fn get_all_payjoin_senders( + fn payjoin_get_all_sender_sessions( &mut self, - ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { - self.get_all_payjoin_senders() + ) -> Vec<(SessionId, SessionWrapper)> { + self.payjoin_get_all_sender_sessions() } fn update_payjoin_sender_status( &mut self, - spend_tx_id: bitcoin::Txid, - status: PayjoinSenderStatus, - maybe_sender: Option, - maybe_new_txid: Option, + session_id: &SessionId, + session: SessionWrapper, ) { - self.update_payjoin_sender_status(spend_tx_id, status, maybe_sender, maybe_new_txid) + self.update_payjoin_sender_status(session_id, session) } } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 30d2e8944..1c69b0bf1 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -25,17 +25,16 @@ use crate::{ }, Coin, CoinStatus, LabelItem, }, - payjoin::types::{PayjoinReceiverStatus, PayjoinSenderStatus}, + payjoin::db::{SessionId, SessionWrapper}, }; use liana::descriptors::LianaDescriptor; -use payjoin::{bitcoin::Txid, receive::v2::Receiver, send::v2::Sender}; +use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent}; use std::{ cmp, collections::{HashMap, HashSet}, convert::TryInto, fmt, io, path, - str::FromStr, }; use miniscript::bitcoin::{ @@ -967,120 +966,147 @@ impl SqliteConn { .expect("Db must not fail"); } - /// Create a payjoin receiver, TODO: strong type to bitcoin::Address? - pub fn create_payjoin_receiver( + /// Create a payjoin receiver + pub fn payjoin_next_id(&mut self) -> u64 { + let count = db_query( + &mut self.conn, + "SELECT COUNT(*) FROM payjoin_receivers", + rusqlite::params![], + |row| { + let count: u64 = row.get(0)?; + Ok(count) + }, + ) + .expect("Db must not fail"); + if let Some(count) = count.first() { + return *count; + } else { + return 0; + } + } + + /// Create new Receiver Session + pub fn payjoin_save_receiver_session( &mut self, - address: &bitcoin::Address, - receiver: Receiver, - psbt_str: String, + session_id: &SessionId, + session: SessionWrapper, ) { - let receiver_json = serde_json::to_string(&receiver).unwrap(); + let session_ser = serde_json::to_vec(&session).unwrap(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_receivers (address, status, receiver, psbt) VALUES (?1, ?2, ?3, ?4)", - rusqlite::params![ - address.to_string(), - PayjoinReceiverStatus::Pending as i32, - receiver_json, - psbt_str - ], + "INSERT INTO payjoin_receivers (session_id, session) VALUES (?1, ?2)", + rusqlite::params![session_id.0, session_ser], )?; Ok(()) }) .expect("Db must not fail"); } + /// Get a Receiver Session by Id + pub fn payjoin_get_receiver_session( + &mut self, + session_id: &SessionId, + ) -> Option> { + db_query( + &mut self.conn, + "SELECT session FROM payjoin_receivers WHERE session_id = ?1", + rusqlite::params![session_id.0], + |row| { + let session_ser: Vec = row.get(0)?; + let session: SessionWrapper = + serde_json::from_slice(&session_ser).unwrap(); + Ok(session) + }, + ) + .expect("Db must not fail") + .pop() + } + pub fn update_payjoin_receiver_status( &mut self, - address: &bitcoin::Address, - txid: bitcoin::Txid, - status: PayjoinReceiverStatus, - psbt_str: String, + session_id: &SessionId, + session: SessionWrapper, ) { + let session_ser = serde_json::to_vec(&session).unwrap(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_receivers SET status = ?1, txid = ?2, psbt = ?3 WHERE address = ?4", - rusqlite::params![ - status as i32, - txid[..].to_vec(), - psbt_str, - address.to_string() - ], + "UPDATE payjoin_receivers SET session = ?1 WHERE session_id = ?2", + rusqlite::params![session_ser, session_id.0], )?; Ok(()) }) .expect("Db must not fail"); } - pub fn get_all_payjoin_receivers( + pub fn payjoin_get_all_receiver_sessions( &mut self, - ) -> Vec<( - bitcoin::Address, - bitcoin::Txid, - PayjoinReceiverStatus, - Receiver, - String, - )> { + ) -> Vec<(SessionId, SessionWrapper)> { db_query( &mut self.conn, - "SELECT address, txid, status, receiver, psbt FROM payjoin_receivers", + "SELECT session_id, session FROM payjoin_receivers", rusqlite::params![], |row| { - let address_str: String = row.get(0)?; - let address = bitcoin::Address::from_str(&address_str) - .unwrap() - .assume_checked(); - let txid_str: String = match row.get(1) { - Ok(txid_str) => txid_str, - Err(_) => String::new(), - }; - let txid = match Txid::from_str(&txid_str) { - Ok(txid) => txid, - Err(_) => Txid::all_zeros(), - }; - let status: i32 = row.get(2)?; - let receiver_json: String = row.get(3)?; - let receiver: Receiver = serde_json::from_str(&receiver_json).unwrap(); - let psbt_str: String = row.get(4)?; - Ok((address, txid, status.into(), receiver, psbt_str)) + let id: u64 = row.get(0)?; + let session_id = SessionId::new(id); + let session_ser: Vec = row.get(1)?; + let session: SessionWrapper = + serde_json::from_slice(&session_ser).unwrap(); + Ok((session_id, session)) }, ) .expect("Db must not fail") } /// Create a payjoin sender - pub fn create_payjoin_sender(&mut self, bip21: String, txid: bitcoin::Txid) { - let status = PayjoinSenderStatus::Pending; + pub fn payjoin_save_sender_session( + &mut self, + session_id: &SessionId, + session: SessionWrapper, + ) { + let session_ser = serde_json::to_vec(&session).unwrap(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_senders (bip21, txid, status, sender) VALUES (?1, ?2, ?3, ?4)", - rusqlite::params![bip21, txid[..].to_vec(), status as i32, String::new()], + "INSERT INTO payjoin_senders (session_id, session) VALUES (?1, ?2)", + rusqlite::params![session_id.0, session_ser], )?; Ok(()) }) .expect("Db must not fail"); } - pub fn get_all_payjoin_senders( + pub fn payjoin_get_sender_session( &mut self, - ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { + session_id: &SessionId, + ) -> Option> { db_query( &mut self.conn, - "SELECT bip21, txid, status, sender FROM payjoin_senders", + "SELECT session FROM payjoin_senders WHERE session_id = ?1", + rusqlite::params![session_id.0], + |row| { + let session_ser: Vec = row.get(0)?; + let session: SessionWrapper = + serde_json::from_slice(&session_ser).unwrap(); + Ok(session) + }, + ) + .expect("Db must not fail") + .pop() + } + + pub fn payjoin_get_all_sender_sessions( + &mut self, + ) -> Vec<(SessionId, SessionWrapper)> { + db_query( + &mut self.conn, + "SELECT session_id, session FROM payjoin_senders", rusqlite::params![], |row| { - let bip21: String = row.get(0)?; - let spend_tx_id: Vec = row.get(1)?; - let txid: bitcoin::Txid = - encode::deserialize(&spend_tx_id).expect("We only store valid txids"); - let status: i32 = row.get(2)?; - let sender_json: String = row.get(3)?; - let maybe_sender: Option = if !sender_json.is_empty() { - Some(serde_json::from_str(&sender_json).unwrap()) - } else { - None - }; - Ok((bip21, txid, PayjoinSenderStatus::from(status), maybe_sender)) + let id: u64 = row.get(0)?; + let session_id = SessionId::new(id); + let session_ser: Vec = row.get(1)?; + let session: SessionWrapper = + serde_json::from_slice(&session_ser).unwrap(); + Ok((session_id, session)) }, ) .expect("Db must not fail") @@ -1088,32 +1114,18 @@ impl SqliteConn { pub fn update_payjoin_sender_status( &mut self, - txid: bitcoin::Txid, - status: PayjoinSenderStatus, - maybe_sender: Option, - maybe_new_txid: Option, + session_id: &SessionId, + session: SessionWrapper, ) { - if let Some(sender) = maybe_sender { - let sender_json = serde_json::to_string(&sender).unwrap(); - db_exec(&mut self.conn, |db_tx| { - db_tx.execute( - "UPDATE payjoin_senders SET status = ?1, sender = ?2 WHERE txid = ?3", - rusqlite::params![status as i32, sender_json, txid[..].to_vec()], - )?; - Ok(()) - }) - .expect("Db must not fail"); - } else { - let new_txid = maybe_new_txid.unwrap(); - db_exec(&mut self.conn, |db_tx| { - db_tx.execute( - "UPDATE payjoin_senders SET status = ?1, sender = ?2, txid = ?3 WHERE txid = ?4", - rusqlite::params![status as i32, String::new(), new_txid[..].to_vec(), txid[..].to_vec()], - )?; - Ok(()) - }) - .expect("Db must not fail"); - } + let session_ser = serde_json::to_vec(&session).unwrap(); + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_senders SET session = ?1 WHERE session_id = ?2", + rusqlite::params![session_ser, session_id.0], + )?; + Ok(()) + }) + .expect("Db must not fail"); } } diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 3c78d560f..7f2221e11 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -126,20 +126,15 @@ CREATE TABLE labels ( /* Payjoin senders */ CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, - bip21 TEXT NOT NULL, - txid BLOB UNIQUE NOT NULL, - status INTEGER NOT NULL CHECK (status IN (0,1,2,3)), - sender TEXT NOT NULL + session_id INTEGER NOT NULL, + session BLOB NOT NULL ); /* Payjoin receivers */ CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, - address TEXT UNIQUE NOT NULL, - txid BLOB, - status INTEGER NOT NULL CHECK (status IN (0,1,2,3)), - receiver TEXT NOT NULL, - psbt TEXT NOT NULL + session_id INTEGER NOT NULL, + session BLOB NOT NULL ); "; diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs new file mode 100644 index 000000000..a79e4ec6a --- /dev/null +++ b/lianad/src/payjoin/db.rs @@ -0,0 +1,183 @@ +use payjoin::bitcoin::{Psbt, Txid}; +use payjoin::persist::PersistedSession; +use payjoin::receive::v2::ReceiverSessionEvent; +use payjoin::send::v2::SenderSessionEvent; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::sync::Arc; +use std::time::SystemTime; + +use crate::database::DatabaseInterface; + +use super::types::PayjoinStatus; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionWrapper { + pub txid: Option, + pub psbt: Option, + pub status: PayjoinStatus, + pub bip21: Option, + pub events: Vec, + pub completed_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionId(pub u64); + +impl SessionId { + pub fn new(id: u64) -> Self { + Self(id) + } +} + +#[derive(Debug)] +pub enum PersisterError { + Serialize(serde_json::Error), + Deserialize(serde_json::Error), + NotFound(String), +} + +impl Display for PersisterError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + PersisterError::Serialize(e) => write!(f, "Serialization failed: {e}"), + PersisterError::Deserialize(e) => write!(f, "Deserialization failed: {e}"), + PersisterError::NotFound(key) => write!(f, "Key not found: {key}"), + } + } +} + +impl std::error::Error for PersisterError {} + +#[derive(Clone)] +pub struct ReceiverPersister { + db: Arc, + pub session_id: SessionId, +} + +impl ReceiverPersister { + pub fn new(db: Arc) -> Result { + let mut db_conn = db.connection(); + let session_id = SessionId::new(db_conn.payjoin_next_id()); + let session: SessionWrapper = SessionWrapper { + txid: None, + psbt: None, + status: PayjoinStatus::Pending, + bip21: None, + events: vec![], + completed_at: None, + }; + db_conn.payjoin_save_receiver_session(&session_id, session); + Ok(Self { db, session_id }) + } + + pub fn from_id(db: Arc, id: SessionId) -> Result { + Ok(Self { db, session_id: id }) + } +} + +impl PersistedSession for ReceiverPersister { + type SessionEvent = ReceiverSessionEvent; + type InternalStorageError = PersisterError; + + fn save_event( + &self, + event: &Self::SessionEvent, + ) -> std::result::Result<(), Self::InternalStorageError> { + let mut db_conn = self.db.connection(); + // Check if key exists + if let Some(mut session) = db_conn.payjoin_get_receiver_session(&self.session_id) { + // Append new event + session.events.push(event.clone()); + db_conn.update_payjoin_receiver_status(&self.session_id, session); + } + Ok(()) + } + + fn load( + &self, + ) -> std::result::Result>, Self::InternalStorageError> + { + let mut db_conn = self.db.connection(); + let session = db_conn + .payjoin_get_receiver_session(&self.session_id) + .expect("key should exist"); + Ok(Box::new(session.events.into_iter())) + } + + fn close(&self) -> std::result::Result<(), Self::InternalStorageError> { + let mut db_conn = self.db.connection(); + if let Some(mut session) = db_conn.payjoin_get_receiver_session(&self.session_id) { + session.completed_at = Some(SystemTime::now()); + db_conn.update_payjoin_receiver_status(&self.session_id, session); + } + Ok(()) + } +} + +#[derive(Clone)] +pub struct SenderPersister { + db: Arc, + pub session_id: SessionId, +} + +impl SenderPersister { + pub fn new(db: Arc) -> Result { + let mut db_conn = db.connection(); + let session_id = SessionId::new(db_conn.payjoin_next_id()); + let session: SessionWrapper = SessionWrapper { + txid: None, + psbt: None, + status: PayjoinStatus::Pending, + bip21: None, + events: vec![], + completed_at: None, + }; + db_conn.payjoin_save_sender_session(&session_id, session); + Ok(Self { db, session_id }) + } + + pub fn from_id(db: Arc, id: SessionId) -> Result { + Ok(Self { db, session_id: id }) + } +} + +impl PersistedSession for SenderPersister { + type SessionEvent = SenderSessionEvent; + type InternalStorageError = PersisterError; + + fn save_event( + &self, + event: &Self::SessionEvent, + ) -> std::result::Result<(), Self::InternalStorageError> { + let mut db_conn = self.db.connection(); + // Check if key exists + if let Some(mut session) = db_conn.payjoin_get_sender_session(&self.session_id) { + // Append new event + session.events.push(event.clone()); + db_conn.update_payjoin_sender_status(&self.session_id, session); + } + Ok(()) + } + + fn load( + &self, + ) -> std::result::Result>, Self::InternalStorageError> + { + let mut db_conn = self.db.connection(); + let session = db_conn + .payjoin_get_sender_session(&self.session_id) + .expect("key should exist"); + Ok(Box::new(session.events.into_iter())) + } + + fn close(&self) -> std::result::Result<(), Self::InternalStorageError> { + let mut db_conn = self.db.connection(); + if let Some(mut session) = db_conn.payjoin_get_sender_session(&self.session_id) { + session.completed_at = Some(SystemTime::now()); + db_conn.update_payjoin_sender_status(&self.session_id, session); + } + Ok(()) + } +} diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 1d1b2a526..3bd221c9b 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -1,14 +1,11 @@ -use std::error::Error; +use std::{error::Error, time::Duration}; use miniscript::{ bitcoin::{secp256k1, Psbt, ScriptBuf, TxOut}, psbt::PsbtExt, }; -use payjoin::{ - bitcoin::Amount, - receive::v2::{UncheckedProposal, WantsOutputs}, -}; +use payjoin::bitcoin::Amount; pub const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; @@ -22,6 +19,7 @@ pub fn post_request(req: payjoin::Request) -> Result Ok(r), @@ -62,20 +60,3 @@ pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 WantsOutputs { - let proposal = proposal - .clone() - .check_broadcast_suitability(None, |_| Ok(true)) - .expect("Failed to check broadcast suitability"); - let proposal = proposal - .check_inputs_not_owned(|_input| Ok(false)) - .expect("Failed to check inputs not owned"); - let proposal = proposal - .check_no_inputs_seen_before(|_| Ok(false)) - .expect("Failed to check no inputs seen before"); - let proposal = proposal - .identify_receiver_outputs(|_| Ok(true)) - .expect("Failed to identify receiver outputs"); - proposal -} diff --git a/lianad/src/payjoin/mod.rs b/lianad/src/payjoin/mod.rs index c69b87879..2916f0851 100644 --- a/lianad/src/payjoin/mod.rs +++ b/lianad/src/payjoin/mod.rs @@ -1,3 +1,4 @@ +pub mod db; mod helpers; pub mod receiver; pub mod sender; diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index a054b0f08..8432a0d08 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -1,33 +1,44 @@ -use std::{collections::HashMap, str::FromStr}; +use std::{ + collections::HashMap, + error::Error, + sync::{self, Arc}, +}; use liana::descriptors; use payjoin::{ bitcoin::{psbt::Input, secp256k1, FeeRate, OutPoint, Psbt, Sequence, TxIn}, - receive::{v2::UncheckedProposal, InputPair}, - Url, + persist::{NoopPersister, PersistedError, PersistedSucccessWithMaybeNoResults}, + receive::{ + v2::{ + replay_receiver_event_log, Receiver, ReceiverSessionEvent, ReceiverState, + ReceiverWithContext, UncheckedProposal, + }, + InputPair, + }, }; use crate::{ - database::{Coin, CoinStatus, DatabaseConnection}, - payjoin::helpers::{finalize_psbt, post_request, proposal_dummy_checks_bypass, OHTTP_RELAY}, + database::{Coin, CoinStatus, DatabaseConnection, DatabaseInterface}, + payjoin::helpers::{finalize_psbt, post_request, OHTTP_RELAY}, }; -use super::types::PayjoinReceiverStatus; +use super::{ + db::{ReceiverPersister, SessionId, SessionWrapper}, + types::PayjoinStatus, +}; -pub fn process_proposal_psbt( - proposal: &UncheckedProposal, +fn handle_directory_proposal( + proposal: Receiver, + persister: &ReceiverPersister, db_conn: &mut Box, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, -) -> Result { +) -> Result> { // descs must always have 2 descriptors assert_eq!(descs.len(), 2); let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); - let proposal = proposal_dummy_checks_bypass(&proposal); - let proposal = proposal.commit_outputs(); - let mut candidate_inputs_map = HashMap::::new(); for (outpoint, coin) in coins.iter() { let txs = db_conn.list_wallet_transactions(&[outpoint.txid]); @@ -35,7 +46,7 @@ pub fn process_proposal_psbt( let tx = db_tx.clone(); - let txout = tx.tx_out(outpoint.vout as usize).unwrap().clone(); + let txout = tx.tx_out(outpoint.vout as usize)?.clone(); let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); @@ -60,20 +71,52 @@ pub fn process_proposal_psbt( .values() .map(|(_, txin, psbtin)| InputPair::new(txin.clone(), psbtin.clone()).unwrap()); - let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); + // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx + let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); + + // Receive Check 1: Can Broadcast + let proposal = proposal + .check_broadcast_suitability(None, |_| Ok(true)) + .save(persister)?; + + // Receive Check 2: receiver can't sign for proposal inputs + let proposal = proposal + .check_inputs_not_owned(|_input| Ok(false)) + .save(persister)?; + + // Receive Check 3: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. + let proposal = proposal + .check_no_inputs_seen_before(|_| Ok(false)) + .save(persister)?; + + // Receive Check 4: identify receiver outputs + let proposal = proposal + .identify_receiver_outputs(|_| Ok(true)) + .save(persister)?; + + // Receive Check 5: commit outputs + let proposal = proposal.commit_outputs().save(persister)?; - let proposal = proposal.contribute_inputs(vec![selected_input]).unwrap(); - let proposal = proposal.commit_inputs(); + let selected_input = proposal.try_preserving_privacy(candidate_inputs)?; let proposal = proposal - .finalize_proposal( - |psbt| Ok(psbt.clone()), - None, - Some(FeeRate::from_sat_per_vb(150).unwrap()), - ) - .expect("Failed to finalize proposal"); + .contribute_inputs(vec![selected_input]) + .map_err(|e| format!("Failed to contribute inputs: {e:?}"))? + .commit_inputs() + .save(persister)?; - let mut psbt = proposal.psbt().clone(); + // Extract + let proposal = proposal.finalize_proposal( + |psbt: &Psbt| Ok(psbt.clone()), + Some(FeeRate::BROADCAST_MIN), + Some(FeeRate::from_sat_per_vb_unchecked(2)), + ); + + // TODO(arturgontijo): Avoiding changing state, for now + let noop_persister = NoopPersister::default(); + let payjoin_proposal = proposal.save(&noop_persister)?; + + let mut psbt = payjoin_proposal.psbt().clone(); // TODO(arturgontijo): If we use a previous payjoin utxo it is breaking while broadcasting for (index, psbtin) in psbt.inputs.iter_mut().enumerate() { @@ -89,150 +132,208 @@ pub fn process_proposal_psbt( Ok(psbt) } -pub fn payjoin_receiver_check( +fn poll_fallback( + receiver: Receiver, + persister: &ReceiverPersister, +) -> Result, Box> { + let mut receiver = receiver; + let (req, context) = receiver + .extract_req(OHTTP_RELAY) + .expect("Failed to extract request"); + match post_request(req.clone()) { + Ok(ohttp_response) => { + let state_transition = receiver + .process_res( + ohttp_response + .bytes() + .expect("Failed to read response") + .as_ref(), + context, + ) + .save(persister); + match state_transition { + Ok(PersistedSucccessWithMaybeNoResults::Success(next_state)) => Ok(next_state), + Ok(PersistedSucccessWithMaybeNoResults::NoResults(_current_state)) => { + Err("NoResults".into()) + } + Err(e) => match e { + PersistedError::BadInitInputs(e) + | PersistedError::Fatal(e) + | PersistedError::Transient(e) => Err(e.into()), + PersistedError::Storage(e) => Err(e.into()), + }, + } + } + Err(e) => Err(e.into()), + } +} + +fn process_receiver_session( + session_id: SessionId, + session: SessionWrapper, + state: ReceiverState, + persister: ReceiverPersister, db_conn: &mut Box, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, -) { - let mut payjoin_receivers = db_conn.get_all_payjoin_receivers(); - for (address, _, status, receiver, psbt) in payjoin_receivers.iter_mut() { - match status { - PayjoinReceiverStatus::Pending => { - log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); - let (req, ctx) = receiver - .extract_req(OHTTP_RELAY) - .expect("Failed to extract request"); - match post_request(req.clone()) { - Ok(resp) => { - let proposal = receiver - .process_res( - resp.bytes().expect("Failed to read response").as_ref(), - ctx, - ) - .expect("Failed to process response"); - if let Some(proposal) = proposal { - log::info!("[Payjoin] receiver got a proposal..."); - - let new_psbt = - process_proposal_psbt(&proposal, db_conn, descs, secp).unwrap(); - - // DB logic - log::info!("[Payjoin] Inserting PSBT and STATUS..."); - db_conn.update_payjoin_receiver_status( - &address, - new_psbt.unsigned_tx.compute_txid(), - PayjoinReceiverStatus::Signing, - new_psbt.to_string(), - ); - db_conn.store_spend(&new_psbt); - - log::info!("[Payjoin] Payjoin PSBT in DB!"); - } else { - log::info!("empty proposal") +) -> Result<(), Box> { + match state { + ReceiverState::WithContext(receiver) => { + let bip21 = receiver.pj_uri().to_string(); + log::info!("[Payjoin] ReceiverState::WithContext: {bip21}"); + if session.status == PayjoinStatus::Pending { + match poll_fallback(receiver, &persister) { + Ok(proposal) => { + let psbt = + handle_directory_proposal(proposal, &persister, db_conn, descs, secp)?; + db_conn.store_spend(&psbt); + // TODO(arturgontijo): Need to refetch it to get latest events. + if let Some(mut session) = db_conn.payjoin_get_receiver_session(&session_id) + { + session.status = PayjoinStatus::Signing; + session.bip21 = Some(bip21); + session.txid = Some(psbt.unsigned_tx.compute_txid()); + session.psbt = Some(psbt); + db_conn.update_payjoin_receiver_status(&session_id, session); } + log::info!("ReceiverState::WithContext: PSBT in DB..."); } - Err(err) => { - log::warn!("[Payjoin] payjoin_receiver_check(): {} -> {}", req.url, err) - } + Err(_) => {} } } - PayjoinReceiverStatus::Signing => { - log::info!("[Payjoin] {:?}: {}", status, receiver.pj_uri()); - let psbt = match Psbt::from_str(psbt) { - Ok(psbt) => psbt, - Err(err) => { - log::error!( - "[Payjoin] payjoin_receiver_check(psbt_from): len={} -> {}", - psbt.len(), - err - ); - continue; - } - }; - let txid = psbt.unsigned_tx.compute_txid(); - match db_conn.spend_tx(&txid) { - Some(psbt) => { - let mut is_signed = false; - for psbtin in &psbt.inputs { - if !psbtin.partial_sigs.is_empty() { - log::info!("PSBT was signed!"); - is_signed = true; - break; - } - } + Ok(()) + } + ReceiverState::UncheckedProposal(_proposal) => { + log::info!("ReceiverState::UncheckedProposal"); + Ok(()) + } + ReceiverState::MaybeInputsOwned(_proposal) => { + log::info!("ReceiverState::MaybeInputsOwned"); + Ok(()) + } + ReceiverState::MaybeInputsSeen(_proposal) => { + log::info!("ReceiverState::MaybeInputsSeen"); + Ok(()) + } + ReceiverState::OutputsUnknown(_proposal) => { + log::info!("ReceiverState::OutputsUnknown"); + Ok(()) + } + ReceiverState::WantsOutputs(_proposal) => { + log::info!("ReceiverState::WantsOutputs"); + Ok(()) + } + ReceiverState::WantsInputs(_proposal) => { + log::info!("ReceiverState::WantsInputs"); + Ok(()) + } + ReceiverState::ProvisionalProposal(_proposal) => { + log::info!("ReceiverState::ProvisionalProposal"); + Ok(()) + } + ReceiverState::PayjoinProposal(_proposal) => { + log::info!("ReceiverState::PayjoinProposal"); + Ok(()) + } + _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), + } +} - if is_signed { - let (req, ctx) = receiver - .extract_req( - Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"), - ) - .expect("Failed to extract request"); - match post_request(req.clone()) { - Ok(resp) => { - let proposal = receiver - .process_res( - resp.bytes().expect("Failed to read response").as_ref(), - ctx, - ) - .expect("Failed to process response"); - if let Some(proposal) = proposal { - log::info!("[Payjoin] receiver got a proposal..."); +pub fn payjoin_receiver_check( + db: &sync::Arc>, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) { + let mut db_conn = db.connection(); + for (session_id, session) in db_conn.payjoin_get_all_receiver_sessions() { + log::info!("[Payjoin] {:?}: bip21={:?}", session.status, session.bip21); + let persister = + ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); + let (state, _) = replay_receiver_event_log(persister.clone()) + .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) + .unwrap(); + match session.status { + PayjoinStatus::Pending => { + match process_receiver_session( + session_id, + session, + state, + persister, + &mut db_conn, + descs, + secp, + ) { + Ok(_) => (), + Err(e) => log::warn!("payjoin_receiver_check(): {}", e), + } + } + PayjoinStatus::Signing => { + if let Some(txid) = session.txid { + match db_conn.spend_tx(&txid) { + Some(psbt) => { + let mut is_signed = false; + for psbtin in &psbt.inputs { + if !psbtin.partial_sigs.is_empty() { + log::info!("PSBT was signed!"); + is_signed = true; + break; + } + } + if is_signed { + match state { + ReceiverState::ProvisionalProposal(proposal) => { let mut psbt = psbt.clone(); finalize_psbt(&mut psbt, secp); - let proposal = proposal_dummy_checks_bypass(&proposal); - let proposal = proposal.commit_outputs(); - let proposal = proposal.commit_inputs(); - let mut proposal = proposal .finalize_proposal( |_| Ok(psbt.clone()), None, - Some(FeeRate::from_sat_per_vb(150).unwrap()), + Some(FeeRate::from_sat_per_vb(5).unwrap()), ) - .expect("Failed to finalize proposal"); + .save(&persister) + .unwrap(); let (req, ctx) = proposal - .extract_req( - Url::from_str(OHTTP_RELAY) - .expect("Invalid OHTTP relay"), - ) + .extract_req(OHTTP_RELAY) .expect("Failed to extract request"); // Respond to sender log::info!("[Payjoin] receiver responding to sender..."); + log::info!( + "[Payjoin] DEBUG: post_psbt_proposal(): {}", + req.url + ); match post_request(req.clone()) { Ok(resp) => { - let _proposal = receiver + proposal .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) - .expect("Failed to process response"); - // Update status of receiver - db_conn.update_payjoin_receiver_status( - &address, - psbt.unsigned_tx.compute_txid(), - PayjoinReceiverStatus::Completed, - psbt.to_string(), - ); + .save(&persister) + .unwrap(); + + // TODO(arturgontijo): Need to refetch it to get latest events. + if let Some(mut session) = db_conn.payjoin_get_receiver_session(&session_id) { + // Update status of receiver + session.status = PayjoinStatus::Completed; + db_conn.update_payjoin_receiver_status( + &session_id, + session, + ); + } }, Err(err) => log::error!( "[Payjoin] payjoin_receiver_check(respond_to_sender): {} -> {}", req.url, err ), } - } else { - log::info!("empty proposal") } + _ => {} } - Err(err) => log::warn!( - "[Payjoin] payjoin_receiver_check(Signed): {} -> {}", - req.url, - err - ), } } + None => {} } - None => {} } } _ => {} diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index d784aa7b2..81b4d6961 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -1,165 +1,145 @@ -use crate::database::DatabaseConnection; +use crate::database::DatabaseInterface; -use crate::payjoin::{helpers::post_request, types::PayjoinSenderStatus}; +use crate::payjoin::helpers::post_request; -use std::{collections::HashMap, convert::TryFrom, str::FromStr}; +use std::error::Error; +use std::sync::{self, Arc}; -use payjoin::{ - bitcoin::{psbt::Input, FeeRate, OutPoint}, - persist::NoopPersister, - send::v2::{Sender, SenderBuilder}, - Uri, UriExt, Url, -}; +use payjoin::bitcoin::Psbt; +use payjoin::persist::PersistedSucccessWithMaybeNoResults; +use payjoin::send::v2::Sender; +use payjoin::send::v2::{replay_sender_event_log, SenderState, SenderWithReplyKey, V2GetContext}; +use super::db::SenderPersister; use super::helpers::OHTTP_RELAY; - -pub fn payjoin_sender_check(db_conn: &mut Box) { - let ohttp_url = Url::from_str(OHTTP_RELAY).expect("Invalid OHTTP relay"); - let payjoin_senders = db_conn.get_all_payjoin_senders(); - for (bip21, txid, status, maybe_sender) in payjoin_senders { - match status { - PayjoinSenderStatus::Pending => { - log::info!("[Payjoin] PayjoinSenderStatus: {:?} | {}", status, bip21); - - if let Some(psbt) = db_conn.spend_tx(&txid) { - let pj_uri = Uri::try_from(bip21.as_str()) - .expect("Invalid BIP21") - .assume_checked() - .check_pj_supported() - .expect("Invalid PJ BIP21"); - - let new_sender = SenderBuilder::new(psbt, pj_uri) - .build_recommended(FeeRate::BROADCAST_MIN) - .expect("Failed to build sender"); - - // TODO: should just be able to load a sender from the db, and not use the NoopPersister. - let storage_token = new_sender - .persist(&mut NoopPersister) - .expect("Failed to persist sender"); - - let sender = - Sender::load(storage_token, &NoopPersister).expect("Failed to load sender"); - - let (post_req, _) = sender - .extract_v2(ohttp_url.clone()) - .expect("Failed to extract v2"); - // Send original PSBT to the receiver via the BIP77 directory - match post_request(post_req.clone()) { - Ok(_) => { - log::info!("[Payjoin] Updating PSBT's STATUS..."); - db_conn.update_payjoin_sender_status( - txid, - PayjoinSenderStatus::WaitingReceiver, - Some(sender), - None, - ); - } - Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), - } +use super::types::PayjoinStatus; + +fn get_proposed_payjoin_psbt( + context: Sender, + persister: &SenderPersister, +) -> Result, Box> { + let (req, ctx) = context.extract_req(OHTTP_RELAY)?; + log::info!("[Payjoin] DEBUG: get_proposed_payjoin_psbt(): {}", req.url); + match post_request(req) { + Ok(resp) => { + let res = context + .process_response( + &resp.bytes().expect("Failed to read response").as_ref(), + ctx, + ) + .save(persister); + match res { + Ok(PersistedSucccessWithMaybeNoResults::Success(proposal)) => { + let psbt = proposal.psbt(); + log::info!("[Payjoin] Proposal received. PSBT: {}", psbt.to_string()); + Ok(Some(psbt.clone())) + } + Ok(PersistedSucccessWithMaybeNoResults::NoResults(_current_state)) => { + log::info!("[Payjoin] No response yet."); + // context = current_state; + Ok(None) + } + Err(e) => { + log::error!("{:?}", e); + Err(format!("Response error: {}", e).into()) } } - PayjoinSenderStatus::WaitingReceiver => { - log::info!( - "[Payjoin] PayjoinSenderStatus: {:?} | sender_is_set={}", - status, - maybe_sender.is_some() - ); - if let Some(sender) = maybe_sender { - let (post_req, post_ctx) = sender - .extract_v2(ohttp_url.clone()) - .expect("Failed to extract v2"); - - match post_request(post_req.clone()) { - Ok(resp) => { - let get_ctx = post_ctx - .process_response( - resp.bytes().expect("Must be valid response").as_ref(), - ) - .expect("Failed to process response"); - - // Read the response from the receiver via the BIP77 directory - let (get_req, ohttp_ctx) = get_ctx - .extract_req(OHTTP_RELAY) - .expect("Failed to extract get request"); - - if let Some(psbt) = db_conn.spend_tx(&txid) { - // TODO(arturgontijo): PDK removes fields that we need in the GUI to properly sign the inputs - let mut input_fields_to_restore = HashMap::::new(); - for (index, txin) in psbt.unsigned_tx.input.iter().enumerate() { - let mut input_without_sigs = psbt.inputs[index].clone(); - input_without_sigs.partial_sigs = Default::default(); - input_fields_to_restore - .insert(txin.previous_output.clone(), input_without_sigs); - } - - match post_request(get_req.clone()) { - Ok(resp) => { - log::info!("Payjoin sender got a final PSBT..."); - - let mut psbt = match get_ctx.process_response( - resp.bytes().expect("Failed to read response").as_ref(), - ohttp_ctx, - ) { - Ok(Some(psbt)) => psbt, - Ok(None) => { - // nothing to do yet, no response - log::warn!("Nothing to do yet, no response..."); - continue; - } - Err(e) => { - log::warn!( - "Failed to process payjoin sender response: {:?}", - e - ); - // TODO: handle error - continue; - } - }; - - // TODO(arturgontijo): Restoring witness_scripts and bip32_derivation so GUI can sign them - for (index, psbtin) in psbt.inputs.iter_mut().enumerate() { - let outpoint = - &psbt.unsigned_tx.input[index].previous_output; - if let Some(input) = - input_fields_to_restore.get(outpoint) - { - *psbtin = input.clone(); - } - } + } + Err(e) => Err(e.into()), + } +} - // Store updated Payjoin psbt - let new_txid = psbt.unsigned_tx.compute_txid(); - log::info!( - "Updated Payjoin psbt: {} -> {}", - txid, - new_txid, - ); - db_conn.store_spend(&psbt); +fn post_orginal_proposal( + sender: Sender, + persister: &SenderPersister, +) -> Result<(), Box> { + let (req, ctx) = sender.extract_v2(OHTTP_RELAY)?; + log::info!("[Payjoin] DEBUG: post_orginal_proposal(): {}", req.url); + match post_request(req) { + Ok(resp) => { + log::info!("[Payjoin] Posted original proposal..."); + sender + .process_response( + &resp.bytes().expect("Failed to read response").as_ref(), + ctx, + ) + .save(persister)?; + Ok(()) + } + Err(e) => Err(e.into()), + } +} - log::info!( - "Deleting original Payjoin psbt (txid={})", - txid - ); - db_conn.delete_spend(&txid); +fn process_sender_session( + state: SenderState, + persister: &SenderPersister, +) -> Result, Box> { + match state { + SenderState::WithReplyKey(sender) => { + log::info!("[Payjoin] SenderState::WithReplyKe"); + match post_orginal_proposal(sender, persister) { + Ok(_) => {} + Err(err) => log::warn!("post_orginal_proposal(): {}", err), + } + return Ok(None); + } + SenderState::V2GetContext(context) => { + log::info!("[Payjoin] SenderState::V2GetContext"); + get_proposed_payjoin_psbt(context, persister) + } + SenderState::ProposalReceived(proposal) => { + log::info!( + "[Payjoin] SenderState::ProposalReceived: {}", + proposal.psbt().to_string() + ); + return Ok(None); + } + _ => return Err(format!("Unexpected sender state").into()), + } +} - // Mark the sender as completed - db_conn.update_payjoin_sender_status( - txid, - PayjoinSenderStatus::Completed, - None, - Some(new_txid), - ); - } - Err(e) => { - log::warn!("Failed to get receiver's proposal: {:?}", e) - } - } +pub fn payjoin_sender_check(db: &sync::Arc>) { + let mut db_conn = db.connection(); + for (session_id, session) in db_conn.payjoin_get_all_sender_sessions() { + log::info!("[Payjoin] {:?}: bip21={:?}", session.status, session.bip21); + let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); + let (state, _) = replay_sender_event_log(persister.clone()) + .map_err(|e| format!("Failed to replay sender event log: {:?}", e)) + .unwrap(); + match session.status { + PayjoinStatus::Pending => match process_sender_session(state, &persister) { + Ok(_) => { + if let Some(mut session) = db_conn.payjoin_get_sender_session(&session_id) { + session.status = PayjoinStatus::WaitingReceiver; + db_conn.update_payjoin_sender_status(&session_id, session); + } + } + Err(e) => log::warn!("process_sender_session(): {}", e), + }, + PayjoinStatus::WaitingReceiver => match process_sender_session(state, &persister) { + Ok(maybe_psbt) => { + if let Some(new_psbt) = maybe_psbt { + if let Some(txid) = session.txid { + if let Some(mut session) = + db_conn.payjoin_get_sender_session(&session_id) + { + log::info!("Deleting original Payjoin psbt (txid={txid})"); + db_conn.delete_spend(&txid); + + let new_txid = new_psbt.unsigned_tx.compute_txid(); + log::info!("Updating Payjoin psbt: {} -> {}", txid, new_txid,); + db_conn.store_spend(&new_psbt); + + session.txid = Some(new_txid); + session.psbt = Some(new_psbt); + session.status = PayjoinStatus::Completed; + db_conn.update_payjoin_sender_status(&session_id, session); } } - Err(e) => log::warn!("Failed to POST original proposal: {:?}", e), } } - } + Err(e) => log::warn!("payjoin_sender_check(): {}", e), + }, _ => {} } } diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index c5d6840bf..ff106616e 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + #[derive(Debug, Clone, Copy, PartialEq)] pub enum PayjoinSenderStatus { Pending = 0, @@ -7,7 +9,7 @@ pub enum PayjoinSenderStatus { Failed = 3, } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PayjoinReceiverStatus { Pending = 0, Signing = 1, @@ -40,9 +42,32 @@ impl From for PayjoinReceiverStatus { } } +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum PayjoinStatus { + Pending = 0, + WaitingReceiver = 1, + Signing = 2, + Completed = 3, + // TODO: more specific enums for why it failed + Failed = 4, +} + +impl From for PayjoinStatus { + fn from(status: i32) -> Self { + match status { + 0 => PayjoinStatus::Pending, + 1 => PayjoinStatus::WaitingReceiver, + 2 => PayjoinStatus::Signing, + 3 => PayjoinStatus::Completed, + 4 => PayjoinStatus::Failed, + _ => panic!("Invalid payjoin status: {}", status), + } + } +} + #[derive(Debug, Clone)] pub struct PayjoinInfo { - pub sender_status: Option, - pub receiver_status: Option, + pub sender_status: Option, + pub receiver_status: Option, pub bip21: String, } diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 08bc34ff0..3d78bae58 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -5,11 +5,11 @@ use crate::{ BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet, }, datadir::DataDirectory, - payjoin::types::{PayjoinReceiverStatus, PayjoinSenderStatus}, + payjoin::db::{SessionId, SessionWrapper}, DaemonControl, DaemonHandle, }; use liana::descriptors; -use payjoin::{receive::v2::Receiver, send::v2::Sender}; +use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent}; use std::convert::TryInto; use std::{ @@ -531,54 +531,65 @@ impl DatabaseConnection for DummyDatabase { todo!() } - fn create_payjoin_receiver( + fn payjoin_get_all_receiver_sessions( &mut self, - _address: &bitcoin::Address, - _receiver: Receiver, - _psbt: String, + ) -> Vec<(SessionId, SessionWrapper)> { + todo!() + } + + fn update_payjoin_receiver_status( + &mut self, + _session_id: &SessionId, + _session: SessionWrapper, ) { todo!() } - fn get_all_payjoin_receivers( + fn payjoin_save_sender_session( &mut self, - ) -> Vec<( - bitcoin::Address, - bitcoin::Txid, - PayjoinReceiverStatus, - Receiver, - String, - )> { + _session_id: &SessionId, + _session: SessionWrapper, + ) { todo!() } - fn update_payjoin_receiver_status( + fn update_payjoin_sender_status( &mut self, - _address: &bitcoin::Address, - _txid: bitcoin::Txid, - _status: PayjoinReceiverStatus, - _psbt_str: String, + _session_id: &SessionId, + _session: SessionWrapper, ) { todo!() } - fn create_payjoin_sender(&mut self, _bip21: String, _spend_tx_id: bitcoin::Txid) { + fn payjoin_next_id(&mut self) -> u64 { todo!() } - fn get_all_payjoin_senders( + fn payjoin_save_receiver_session( &mut self, - ) -> Vec<(String, bitcoin::Txid, PayjoinSenderStatus, Option)> { + _session_id: &SessionId, + _session: SessionWrapper, + ) { todo!() } - fn update_payjoin_sender_status( + fn payjoin_get_receiver_session( &mut self, - _spend_tx_id: bitcoin::Txid, - _status: PayjoinSenderStatus, - _maybe_sender: Option, - _maybe_new_txid: Option, - ) { + _session_id: &SessionId, + ) -> Option> { + todo!() + } + + fn payjoin_get_sender_session( + &mut self, + _session_id: &SessionId, + ) -> Option> { + todo!() + } + + fn payjoin_get_all_sender_sessions( + &mut self, + ) -> Vec<(SessionId, SessionWrapper)> { todo!() } } From b6640833762427269477d6f703aba31240097ce2 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Wed, 4 Jun 2025 16:00:05 -0300 Subject: [PATCH 07/72] [PDK] SEL working... --- liana-gui/src/app/state/psbt.rs | 9 +- liana-gui/src/app/state/spend/step.rs | 9 +- liana-gui/src/app/view/psbt.rs | 18 +- liana-gui/src/daemon/client/mod.rs | 2 +- liana-gui/src/daemon/embedded.rs | 4 +- liana-gui/src/daemon/mod.rs | 4 +- liana-gui/src/daemon/model.rs | 10 +- .../services/connect/client/backend/mod.rs | 2 +- lianad/src/commands/mod.rs | 45 +++-- lianad/src/database/mod.rs | 6 +- lianad/src/database/sqlite/mod.rs | 4 +- lianad/src/database/sqlite/schema.rs | 4 +- lianad/src/payjoin/db.rs | 84 ++++++-- lianad/src/payjoin/helpers.rs | 6 +- lianad/src/payjoin/receiver.rs | 183 ++++++------------ lianad/src/payjoin/sender.rs | 99 +++++++--- lianad/src/testutils.rs | 2 +- 17 files changed, 262 insertions(+), 229 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 8e65bd493..32b52a949 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -12,6 +12,7 @@ use lianad::commands::CoinStatus; use liana_ui::component::toast; use liana_ui::{component::modal, widget::Element}; +use lianad::payjoin::types::PayjoinStatus; use crate::daemon::model::LabelsLoader; use crate::export::{ImportExportMessage, ImportExportType, Progress}; @@ -557,10 +558,10 @@ impl Modal for SignModal { merge_signatures(&mut tx.psbt, &psbt); // TODO(arturgontijo): Use better design. Maybe checking for foreign inputs. - // Payjoin Receiver Side - let psbt_ready = psbt.clone().extract_tx(); - if psbt_ready.is_err() { - tx.status = SpendStatus::PayjoinProposalReady; + if let Some(payjoin_info) = &tx.payjoin_info { + if payjoin_info.receiver_status == Some(PayjoinStatus::Signing) { + tx.status = SpendStatus::PayjoinProposalReady; + } } if self.is_saved { diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index d68434dcb..e3b6c0510 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -18,10 +18,7 @@ use liana::{ }, spend::{SpendCreationError, MAX_FEERATE}, }; -use lianad::{ - commands::ListCoinsEntry, - payjoin::types::{PayjoinInfo, PayjoinStatus}, -}; +use lianad::{commands::ListCoinsEntry, payjoin::types::PayjoinInfo}; use liana_ui::{component::form, widget::Element}; use payjoin::Uri; @@ -976,8 +973,8 @@ impl Step for SaveSpend { } else { Some(PayjoinInfo { bip21, - sender_status: Some(PayjoinStatus::Pending), - receiver_status: Some(PayjoinStatus::Pending), + sender_status: None, + receiver_status: None, }) }; diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index d45e73e35..5faa798a8 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -399,11 +399,19 @@ pub fn spend_overview_view<'a>( ) }) .push_maybe(if tx.path_ready().is_some() { - Some( - button::secondary(None, "Send Payjoin") - .on_press(Message::Spend(SpendTxMessage::SendPayjoin)) - .width(Length::Fixed(150.0)), - ) + if let Some(payjoin_info) = &tx.payjoin_info { + if payjoin_info.sender_status.is_none() { + Some( + button::secondary(None, "Send Payjoin") + .on_press(Message::Spend(SpendTxMessage::SendPayjoin)) + .width(Length::Fixed(150.0)), + ) + } else { + None + } + } else { + None + } } else { None }) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 343b5b1c5..d80df0405 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -101,7 +101,7 @@ impl Daemon for Lianad { unimplemented!() } - async fn get_sender_payjoin(&self, _txid: &Txid) -> Result, DaemonError> { + async fn get_payjoin_info(&self, _txid: &Txid) -> Result, DaemonError> { unimplemented!() } diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index fedaac231..8c3176643 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -123,10 +123,10 @@ impl Daemon for EmbeddedDaemon { .await } - async fn get_sender_payjoin(&self, txid: &Txid) -> Result, DaemonError> { + async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { self.command(|daemon| { daemon - .get_sender_payjoin(txid) + .get_payjoin_info(txid) .map_err(|e| DaemonError::Unexpected(e.to_string())) }) .await diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 89231a159..9ffde64d1 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -112,7 +112,7 @@ pub trait Daemon: Debug { ohttp_keys: OhttpKeys, ) -> Result; async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; - async fn get_sender_payjoin(&self, txid: &Txid) -> Result, DaemonError>; + async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError>; async fn update_deriv_indexes( &self, receive: Option, @@ -208,7 +208,7 @@ pub trait Daemon: Debug { .collect(); let payjoin_info = self - .get_sender_payjoin(&tx.psbt.unsigned_tx.compute_txid()) + .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) .await?; spend_txs.push(model::SpendTx::new( diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index c26999fbd..821d64404 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -149,14 +149,8 @@ impl SpendTx { }; // One input coin is missing, the psbt is deprecated for now. - if coins_map.len() != psbt.inputs.len() { - // TODO(arturgontijo): Skip for now... - log::warn!( - "Not deprecating... coins_map.len({}) != psbt.inputs.len({})", - coins_map.len(), - psbt.inputs.len() - ); - // status = SpendStatus::Deprecated + if coins_map.len() != psbt.inputs.len() && payjoin_info.is_none() { + status = SpendStatus::Deprecated } let sigs = desc diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index 1924f507e..de0e12bd2 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -625,7 +625,7 @@ impl Daemon for BackendWalletClient { unimplemented!() } - async fn get_sender_payjoin(&self, _txid: &Txid) -> Result, DaemonError> { + async fn get_payjoin_info(&self, _txid: &Txid) -> Result, DaemonError> { unimplemented!() } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 2e7093582..b485dccf1 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -9,8 +9,8 @@ use crate::{ database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, payjoin::{ - db::{ReceiverPersister, SenderPersister}, - types::PayjoinInfo, + db::{ReceiverPersister, SenderPersister, SessionMetadata}, + types::{PayjoinInfo, PayjoinStatus}, }, poller::PollerMessage, DaemonControl, VERSION, @@ -48,7 +48,7 @@ use miniscript::{ psbt::PsbtExt, }; use payjoin::{ - bitcoin::{FeeRate, Txid}, + bitcoin::{key::Secp256k1, FeeRate, Txid}, receive::v2::{Receiver, UninitializedReceiver}, send::v2::SenderBuilder, OhttpKeys, Uri, UriExt, Url, @@ -414,34 +414,37 @@ impl DaemonControl { .map_err(|_| format!("URI does not support Payjoin")) .unwrap(); + let mut psbt = psbt.clone(); + psbt.finalize_mut(&Secp256k1::verification_only()).unwrap(); + let persister = SenderPersister::new(Arc::new(self.db.clone())).unwrap(); let _sender = SenderBuilder::new(psbt.clone(), uri) .build_recommended(FeeRate::BROADCAST_MIN) .save(&persister) .unwrap(); - let session_id = persister.session_id; - if let Some(mut session) = self.db.connection().payjoin_get_sender_session(&session_id) { - session.bip21 = Some(bip21); - session.psbt = Some(psbt.clone()); - session.txid = Some(psbt.unsigned_tx.compute_txid()); - self.db - .connection() - .update_payjoin_sender_status(&session_id, session); - } + persister.update_metada( + Some(PayjoinStatus::Pending), + Some(psbt.unsigned_tx.compute_txid()), + Some(psbt.clone()), + Some(bip21.clone()), + ); Ok(()) } /// Get Payjoin URI (BIP21) and its sender/receiver status by txid - pub fn get_sender_payjoin(&self, txid: &Txid) -> Result, CommandError> { + pub fn get_payjoin_info(&self, txid: &Txid) -> Result, CommandError> { let mut db_conn = self.db.connection(); let mut receiver_status = None; for (_, session) in db_conn.payjoin_get_all_receiver_sessions() { - if let Some(db_txid) = session.txid { + let SessionMetadata { + status, maybe_txid, .. + } = session.metadata.clone(); + if let Some(db_txid) = maybe_txid { if &db_txid == txid { - receiver_status = Some(session.status); + receiver_status = Some(status); break; } } @@ -450,10 +453,16 @@ impl DaemonControl { let mut bip21 = String::new(); let mut sender_status = None; for (_, session) in db_conn.payjoin_get_all_sender_sessions() { - if let Some(db_txid) = session.txid { + let SessionMetadata { + status, + maybe_txid, + maybe_bip21, + .. + } = session.metadata.clone(); + if let Some(db_txid) = maybe_txid { if &db_txid == txid { - sender_status = Some(session.status); - bip21 = session.bip21.unwrap_or_default(); + sender_status = Some(status); + bip21 = maybe_bip21.unwrap_or_default(); break; } } diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 854b96484..a78ebfa43 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -200,7 +200,7 @@ pub trait DatabaseConnection { /// Payjoin /// Get the next Session Id - fn payjoin_next_id(&mut self) -> u64; + fn payjoin_next_id(&mut self, table: &str) -> u64; /// Save Receiver Session fn payjoin_save_receiver_session( @@ -475,8 +475,8 @@ impl DatabaseConnection for SqliteConn { .collect() } - fn payjoin_next_id(&mut self) -> u64 { - self.payjoin_next_id() + fn payjoin_next_id(&mut self, table: &str) -> u64 { + self.payjoin_next_id(table) } fn payjoin_save_receiver_session( diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 1c69b0bf1..3520df883 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -967,10 +967,10 @@ impl SqliteConn { } /// Create a payjoin receiver - pub fn payjoin_next_id(&mut self) -> u64 { + pub fn payjoin_next_id(&mut self, table: &str) -> u64 { let count = db_query( &mut self.conn, - "SELECT COUNT(*) FROM payjoin_receivers", + &format!("SELECT COUNT(*) FROM {}", table), rusqlite::params![], |row| { let count: u64 = row.get(0)?; diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 7f2221e11..2a38ad50d 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -126,14 +126,14 @@ CREATE TABLE labels ( /* Payjoin senders */ CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, - session_id INTEGER NOT NULL, + session_id INTEGER UNIQUE NOT NULL, session BLOB NOT NULL ); /* Payjoin receivers */ CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, - session_id INTEGER NOT NULL, + session_id INTEGER UNIQUE NOT NULL, session BLOB NOT NULL ); "; diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index a79e4ec6a..a123156b6 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -1,10 +1,9 @@ use payjoin::bitcoin::{Psbt, Txid}; -use payjoin::persist::PersistedSession; +use payjoin::persist::SessionPersister; use payjoin::receive::v2::ReceiverSessionEvent; use payjoin::send::v2::SenderSessionEvent; use serde::{Deserialize, Serialize}; -use std::fmt; -use std::fmt::{Display, Formatter}; +use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::SystemTime; @@ -13,11 +12,16 @@ use crate::database::DatabaseInterface; use super::types::PayjoinStatus; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionWrapper { - pub txid: Option, - pub psbt: Option, +pub struct SessionMetadata { pub status: PayjoinStatus, - pub bip21: Option, + pub maybe_txid: Option, + pub maybe_psbt: Option, + pub maybe_bip21: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionWrapper { + pub metadata: SessionMetadata, pub events: Vec, pub completed_at: Option, } @@ -59,12 +63,14 @@ pub struct ReceiverPersister { impl ReceiverPersister { pub fn new(db: Arc) -> Result { let mut db_conn = db.connection(); - let session_id = SessionId::new(db_conn.payjoin_next_id()); + let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_receivers")); let session: SessionWrapper = SessionWrapper { - txid: None, - psbt: None, - status: PayjoinStatus::Pending, - bip21: None, + metadata: SessionMetadata { + status: PayjoinStatus::Pending, + maybe_txid: None, + maybe_psbt: None, + maybe_bip21: None, + }, events: vec![], completed_at: None, }; @@ -75,9 +81,28 @@ impl ReceiverPersister { pub fn from_id(db: Arc, id: SessionId) -> Result { Ok(Self { db, session_id: id }) } + + pub fn update_metada( + &self, + status: Option, + maybe_txid: Option, + maybe_psbt: Option, + maybe_bip21: Option, + ) { + let mut db_conn = self.db.connection(); + if let Some(mut session) = db_conn.payjoin_get_receiver_session(&self.session_id) { + session.metadata = SessionMetadata { + status: status.unwrap_or(PayjoinStatus::Pending), + maybe_txid, + maybe_psbt, + maybe_bip21, + }; + db_conn.update_payjoin_receiver_status(&self.session_id, session); + } + } } -impl PersistedSession for ReceiverPersister { +impl SessionPersister for ReceiverPersister { type SessionEvent = ReceiverSessionEvent; type InternalStorageError = PersisterError; @@ -125,12 +150,14 @@ pub struct SenderPersister { impl SenderPersister { pub fn new(db: Arc) -> Result { let mut db_conn = db.connection(); - let session_id = SessionId::new(db_conn.payjoin_next_id()); + let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_senders")); let session: SessionWrapper = SessionWrapper { - txid: None, - psbt: None, - status: PayjoinStatus::Pending, - bip21: None, + metadata: SessionMetadata { + status: PayjoinStatus::Pending, + maybe_txid: None, + maybe_psbt: None, + maybe_bip21: None, + }, events: vec![], completed_at: None, }; @@ -141,9 +168,28 @@ impl SenderPersister { pub fn from_id(db: Arc, id: SessionId) -> Result { Ok(Self { db, session_id: id }) } + + pub fn update_metada( + &self, + status: Option, + maybe_txid: Option, + maybe_psbt: Option, + maybe_bip21: Option, + ) { + let mut db_conn = self.db.connection(); + if let Some(mut session) = db_conn.payjoin_get_sender_session(&self.session_id) { + session.metadata = SessionMetadata { + status: status.unwrap_or(PayjoinStatus::Pending), + maybe_txid, + maybe_psbt, + maybe_bip21, + }; + db_conn.update_payjoin_sender_status(&self.session_id, session); + } + } } -impl PersistedSession for SenderPersister { +impl SessionPersister for SenderPersister { type SessionEvent = SenderSessionEvent; type InternalStorageError = PersisterError; diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 3bd221c9b..fcbd7882f 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -19,7 +19,7 @@ pub fn post_request(req: payjoin::Request) -> Result Ok(r), @@ -51,8 +51,8 @@ pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 log::info!("Finalizing input at: {}", index), - Err(e) => log::warn!("Failed to finalize input at: {} | {}", index, e), + Ok(_) => log::info!("[Payjoin] Finalizing input at: {}", index), + Err(e) => log::warn!("[Payjoin] Failed to finalize input at: {} | {}", index, e), } } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 8432a0d08..85066f7ec 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -7,8 +7,8 @@ use std::{ use liana::descriptors; use payjoin::{ - bitcoin::{psbt::Input, secp256k1, FeeRate, OutPoint, Psbt, Sequence, TxIn}, - persist::{NoopPersister, PersistedError, PersistedSucccessWithMaybeNoResults}, + bitcoin::{psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, TxIn}, + persist::OptionalTransitionOutcome, receive::{ v2::{ replay_receiver_event_log, Receiver, ReceiverSessionEvent, ReceiverState, @@ -20,11 +20,14 @@ use payjoin::{ use crate::{ database::{Coin, CoinStatus, DatabaseConnection, DatabaseInterface}, - payjoin::helpers::{finalize_psbt, post_request, OHTTP_RELAY}, + payjoin::{ + db::SessionMetadata, + helpers::{finalize_psbt, post_request, OHTTP_RELAY}, + }, }; use super::{ - db::{ReceiverPersister, SessionId, SessionWrapper}, + db::{ReceiverPersister, SessionWrapper}, types::PayjoinStatus, }; @@ -34,9 +37,7 @@ fn handle_directory_proposal( db_conn: &mut Box, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, -) -> Result> { - // descs must always have 2 descriptors - assert_eq!(descs.len(), 2); +) -> Result<(), Box> { let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); let mut candidate_inputs_map = HashMap::::new(); @@ -48,7 +49,11 @@ fn handle_directory_proposal( let txout = tx.tx_out(outpoint.vout as usize)?.clone(); - let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); + let receiver_derived_desc = if coin.is_change { + descs[1].derive(coin.derivation_index, secp) + } else { + descs[0].derive(coin.derivation_index, secp) + }; let txin = TxIn { previous_output: outpoint.clone(), @@ -99,37 +104,13 @@ fn handle_directory_proposal( let selected_input = proposal.try_preserving_privacy(candidate_inputs)?; - let proposal = proposal + proposal .contribute_inputs(vec![selected_input]) .map_err(|e| format!("Failed to contribute inputs: {e:?}"))? .commit_inputs() .save(persister)?; - // Extract - let proposal = proposal.finalize_proposal( - |psbt: &Psbt| Ok(psbt.clone()), - Some(FeeRate::BROADCAST_MIN), - Some(FeeRate::from_sat_per_vb_unchecked(2)), - ); - - // TODO(arturgontijo): Avoiding changing state, for now - let noop_persister = NoopPersister::default(); - let payjoin_proposal = proposal.save(&noop_persister)?; - - let mut psbt = payjoin_proposal.psbt().clone(); - - // TODO(arturgontijo): If we use a previous payjoin utxo it is breaking while broadcasting - for (index, psbtin) in psbt.inputs.iter_mut().enumerate() { - let outpoint = &psbt.unsigned_tx.input[index].previous_output; - if let Some((coin, txin, input)) = candidate_inputs_map.get(outpoint) { - *psbtin = input.clone(); - psbt.unsigned_tx.input[index] = txin.clone(); - let receiver_derived_desc = descs[0].derive(coin.derivation_index, secp); - receiver_derived_desc.update_psbt_in(psbtin); - } - } - - Ok(psbt) + Ok(()) } fn poll_fallback( @@ -152,16 +133,9 @@ fn poll_fallback( ) .save(persister); match state_transition { - Ok(PersistedSucccessWithMaybeNoResults::Success(next_state)) => Ok(next_state), - Ok(PersistedSucccessWithMaybeNoResults::NoResults(_current_state)) => { - Err("NoResults".into()) - } - Err(e) => match e { - PersistedError::BadInitInputs(e) - | PersistedError::Fatal(e) - | PersistedError::Transient(e) => Err(e.into()), - PersistedError::Storage(e) => Err(e.into()), - }, + Ok(OptionalTransitionOutcome::Progress(next_state)) => Ok(next_state), + Ok(OptionalTransitionOutcome::Stasis(_current_state)) => Err("NoResults".into()), + Err(e) => return Err(e.into()), } } Err(e) => Err(e.into()), @@ -169,7 +143,6 @@ fn poll_fallback( } fn process_receiver_session( - session_id: SessionId, session: SessionWrapper, state: ReceiverState, persister: ReceiverPersister, @@ -179,62 +152,34 @@ fn process_receiver_session( ) -> Result<(), Box> { match state { ReceiverState::WithContext(receiver) => { - let bip21 = receiver.pj_uri().to_string(); - log::info!("[Payjoin] ReceiverState::WithContext: {bip21}"); - if session.status == PayjoinStatus::Pending { + log::info!("[Payjoin] ReceiverState::WithContext"); + if session.metadata.status == PayjoinStatus::Pending { match poll_fallback(receiver, &persister) { Ok(proposal) => { - let psbt = - handle_directory_proposal(proposal, &persister, db_conn, descs, secp)?; + handle_directory_proposal(proposal, &persister, db_conn, descs, secp)?; + + let (_, history) = replay_receiver_event_log(persister.clone()) + .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) + .unwrap(); + + let psbt = history.psbt_with_contributed_inputs().unwrap(); + let bip21 = history.pj_uri().unwrap().to_string(); + db_conn.store_spend(&psbt); - // TODO(arturgontijo): Need to refetch it to get latest events. - if let Some(mut session) = db_conn.payjoin_get_receiver_session(&session_id) - { - session.status = PayjoinStatus::Signing; - session.bip21 = Some(bip21); - session.txid = Some(psbt.unsigned_tx.compute_txid()); - session.psbt = Some(psbt); - db_conn.update_payjoin_receiver_status(&session_id, session); - } - log::info!("ReceiverState::WithContext: PSBT in DB..."); + log::info!("[Payjoin] ReceiverState::WithContext: PSBT in the DB..."); + + persister.update_metada( + Some(PayjoinStatus::Signing), + Some(psbt.unsigned_tx.compute_txid()), + Some(psbt.clone()), + Some(bip21.clone()), + ); } Err(_) => {} } } Ok(()) } - ReceiverState::UncheckedProposal(_proposal) => { - log::info!("ReceiverState::UncheckedProposal"); - Ok(()) - } - ReceiverState::MaybeInputsOwned(_proposal) => { - log::info!("ReceiverState::MaybeInputsOwned"); - Ok(()) - } - ReceiverState::MaybeInputsSeen(_proposal) => { - log::info!("ReceiverState::MaybeInputsSeen"); - Ok(()) - } - ReceiverState::OutputsUnknown(_proposal) => { - log::info!("ReceiverState::OutputsUnknown"); - Ok(()) - } - ReceiverState::WantsOutputs(_proposal) => { - log::info!("ReceiverState::WantsOutputs"); - Ok(()) - } - ReceiverState::WantsInputs(_proposal) => { - log::info!("ReceiverState::WantsInputs"); - Ok(()) - } - ReceiverState::ProvisionalProposal(_proposal) => { - log::info!("ReceiverState::ProvisionalProposal"); - Ok(()) - } - ReceiverState::PayjoinProposal(_proposal) => { - log::info!("ReceiverState::PayjoinProposal"); - Ok(()) - } _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), } } @@ -246,35 +191,43 @@ pub fn payjoin_receiver_check( ) { let mut db_conn = db.connection(); for (session_id, session) in db_conn.payjoin_get_all_receiver_sessions() { - log::info!("[Payjoin] {:?}: bip21={:?}", session.status, session.bip21); + let SessionMetadata { + status, + maybe_txid, + maybe_bip21, + .. + } = session.metadata.clone(); + + // No need to check Completed + if status == PayjoinStatus::Completed { + continue; + } + + log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); + let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); + let (state, _) = replay_receiver_event_log(persister.clone()) .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) .unwrap(); - match session.status { + + match status { PayjoinStatus::Pending => { - match process_receiver_session( - session_id, - session, - state, - persister, - &mut db_conn, - descs, - secp, - ) { + match process_receiver_session(session, state, persister, &mut db_conn, descs, secp) + { Ok(_) => (), - Err(e) => log::warn!("payjoin_receiver_check(): {}", e), + Err(e) => log::warn!("receiver_check(): {}", e), } } PayjoinStatus::Signing => { - if let Some(txid) = session.txid { + if let Some(txid) = maybe_txid { match db_conn.spend_tx(&txid) { Some(psbt) => { let mut is_signed = false; for psbtin in &psbt.inputs { if !psbtin.partial_sigs.is_empty() { - log::info!("PSBT was signed!"); + log::debug!("[Payjoin] PSBT is signed!"); is_signed = true; break; } @@ -290,7 +243,7 @@ pub fn payjoin_receiver_check( .finalize_proposal( |_| Ok(psbt.clone()), None, - Some(FeeRate::from_sat_per_vb(5).unwrap()), + Some(FeeRate::from_sat_per_vb(150).unwrap()), ) .save(&persister) .unwrap(); @@ -300,11 +253,7 @@ pub fn payjoin_receiver_check( .expect("Failed to extract request"); // Respond to sender - log::info!("[Payjoin] receiver responding to sender..."); - log::info!( - "[Payjoin] DEBUG: post_psbt_proposal(): {}", - req.url - ); + log::info!("[Payjoin] Receiver responding to sender..."); match post_request(req.clone()) { Ok(resp) => { proposal @@ -312,15 +261,7 @@ pub fn payjoin_receiver_check( .save(&persister) .unwrap(); - // TODO(arturgontijo): Need to refetch it to get latest events. - if let Some(mut session) = db_conn.payjoin_get_receiver_session(&session_id) { - // Update status of receiver - session.status = PayjoinStatus::Completed; - db_conn.update_payjoin_receiver_status( - &session_id, - session, - ); - } + persister.update_metada(Some(PayjoinStatus::Completed), maybe_txid, Some(psbt), maybe_bip21); }, Err(err) => log::error!( "[Payjoin] payjoin_receiver_check(respond_to_sender): {} -> {}", diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 81b4d6961..2c047a894 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -1,12 +1,14 @@ use crate::database::DatabaseInterface; +use crate::payjoin::db::SessionMetadata; use crate::payjoin::helpers::post_request; +use std::collections::HashMap; use std::error::Error; use std::sync::{self, Arc}; use payjoin::bitcoin::Psbt; -use payjoin::persist::PersistedSucccessWithMaybeNoResults; +use payjoin::persist::OptionalTransitionOutcome; use payjoin::send::v2::Sender; use payjoin::send::v2::{replay_sender_event_log, SenderState, SenderWithReplyKey, V2GetContext}; @@ -19,7 +21,6 @@ fn get_proposed_payjoin_psbt( persister: &SenderPersister, ) -> Result, Box> { let (req, ctx) = context.extract_req(OHTTP_RELAY)?; - log::info!("[Payjoin] DEBUG: get_proposed_payjoin_psbt(): {}", req.url); match post_request(req) { Ok(resp) => { let res = context @@ -29,14 +30,12 @@ fn get_proposed_payjoin_psbt( ) .save(persister); match res { - Ok(PersistedSucccessWithMaybeNoResults::Success(proposal)) => { - let psbt = proposal.psbt(); - log::info!("[Payjoin] Proposal received. PSBT: {}", psbt.to_string()); - Ok(Some(psbt.clone())) + Ok(OptionalTransitionOutcome::Progress(proposal)) => { + log::info!("[Payjoin] ProposalReceived!"); + Ok(Some(proposal.psbt().clone())) } - Ok(PersistedSucccessWithMaybeNoResults::NoResults(_current_state)) => { + Ok(OptionalTransitionOutcome::Stasis(_current_state)) => { log::info!("[Payjoin] No response yet."); - // context = current_state; Ok(None) } Err(e) => { @@ -54,7 +53,6 @@ fn post_orginal_proposal( persister: &SenderPersister, ) -> Result<(), Box> { let (req, ctx) = sender.extract_v2(OHTTP_RELAY)?; - log::info!("[Payjoin] DEBUG: post_orginal_proposal(): {}", req.url); match post_request(req) { Ok(resp) => { log::info!("[Payjoin] Posted original proposal..."); @@ -76,7 +74,7 @@ fn process_sender_session( ) -> Result, Box> { match state { SenderState::WithReplyKey(sender) => { - log::info!("[Payjoin] SenderState::WithReplyKe"); + log::info!("[Payjoin] SenderState::WithReplyKey"); match post_orginal_proposal(sender, persister) { Ok(_) => {} Err(err) => log::warn!("post_orginal_proposal(): {}", err), @@ -88,11 +86,12 @@ fn process_sender_session( get_proposed_payjoin_psbt(context, persister) } SenderState::ProposalReceived(proposal) => { + let psbt = proposal.psbt(); log::info!( "[Payjoin] SenderState::ProposalReceived: {}", - proposal.psbt().to_string() + psbt.to_string() ); - return Ok(None); + return Ok(Some(psbt.clone())); } _ => return Err(format!("Unexpected sender state").into()), } @@ -101,39 +100,77 @@ fn process_sender_session( pub fn payjoin_sender_check(db: &sync::Arc>) { let mut db_conn = db.connection(); for (session_id, session) in db_conn.payjoin_get_all_sender_sessions() { - log::info!("[Payjoin] {:?}: bip21={:?}", session.status, session.bip21); + let SessionMetadata { + status, + maybe_txid, + maybe_psbt, + maybe_bip21, + } = session.metadata.clone(); + + // No need to check Completed + if status == PayjoinStatus::Completed { + continue; + } + + log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); + let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); + let (state, _) = replay_sender_event_log(persister.clone()) .map_err(|e| format!("Failed to replay sender event log: {:?}", e)) .unwrap(); - match session.status { + + match status { PayjoinStatus::Pending => match process_sender_session(state, &persister) { - Ok(_) => { - if let Some(mut session) = db_conn.payjoin_get_sender_session(&session_id) { - session.status = PayjoinStatus::WaitingReceiver; - db_conn.update_payjoin_sender_status(&session_id, session); - } - } + Ok(_) => persister.update_metada( + Some(PayjoinStatus::WaitingReceiver), + maybe_txid, + maybe_psbt, + maybe_bip21, + ), Err(e) => log::warn!("process_sender_session(): {}", e), }, PayjoinStatus::WaitingReceiver => match process_sender_session(state, &persister) { Ok(maybe_psbt) => { - if let Some(new_psbt) = maybe_psbt { - if let Some(txid) = session.txid { - if let Some(mut session) = - db_conn.payjoin_get_sender_session(&session_id) - { - log::info!("Deleting original Payjoin psbt (txid={txid})"); + if let Some(mut new_psbt) = maybe_psbt { + if let Some(txid) = maybe_txid { + if let Some(psbt) = db_conn.spend_tx(&txid) { + // TODO(arturgontijo): PDK removes fields that we need in the GUI to properly sign the inputs + let mut input_fields_to_restore = HashMap::new(); + for (index, txin) in psbt.unsigned_tx.input.iter().enumerate() { + let mut input_without_sigs = psbt.inputs[index].clone(); + input_without_sigs.partial_sigs = Default::default(); + input_fields_to_restore + .insert(txin.previous_output.clone(), input_without_sigs); + } + log::info!( + "[Payjoin] Deleting original Payjoin psbt (txid={txid})" + ); db_conn.delete_spend(&txid); + // TODO(arturgontijo): Restoring witness_scripts and bip32_derivation so GUI can sign them + for (index, psbtin) in new_psbt.inputs.iter_mut().enumerate() { + let outpoint = + &new_psbt.unsigned_tx.input[index].previous_output; + if let Some(input) = input_fields_to_restore.get(outpoint) { + *psbtin = input.clone(); + } + } + let new_txid = new_psbt.unsigned_tx.compute_txid(); - log::info!("Updating Payjoin psbt: {} -> {}", txid, new_txid,); + log::info!( + "[Payjoin] Updating Payjoin psbt: {} -> {}", + txid, + new_txid, + ); db_conn.store_spend(&new_psbt); - session.txid = Some(new_txid); - session.psbt = Some(new_psbt); - session.status = PayjoinStatus::Completed; - db_conn.update_payjoin_sender_status(&session_id, session); + persister.update_metada( + Some(PayjoinStatus::Completed), + Some(new_txid), + Some(new_psbt), + maybe_bip21, + ); } } } diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 3d78bae58..5dc4f095b 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -561,7 +561,7 @@ impl DatabaseConnection for DummyDatabase { todo!() } - fn payjoin_next_id(&mut self) -> u64 { + fn payjoin_next_id(&mut self, _table: &str) -> u64 { todo!() } From b1c446aa0f809aa8bd48d0f25933ccd007b78743 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Thu, 5 Jun 2025 08:47:15 -0300 Subject: [PATCH 08/72] [PDK] Sync master + OHttpKeys in DB. --- Cargo.lock | 30 ++++--- liana-gui/src/app/state/receive.rs | 22 +---- liana-gui/src/app/view/receive.rs | 87 ++++++++++--------- liana-gui/src/daemon/client/mod.rs | 7 +- liana-gui/src/daemon/embedded.rs | 10 +-- liana-gui/src/daemon/mod.rs | 8 +- .../services/connect/client/backend/mod.rs | 8 +- lianad/src/commands/mod.rs | 29 +++++-- lianad/src/database/mod.rs | 16 +++- lianad/src/database/sqlite/mod.rs | 52 ++++++++++- lianad/src/database/sqlite/schema.rs | 8 ++ lianad/src/payjoin/helpers.rs | 33 ++++++- lianad/src/payjoin/mod.rs | 2 +- lianad/src/testutils.rs | 10 ++- 14 files changed, 208 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d895770a..5935c2bd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2622,11 +2622,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "futures-util", "http 1.3.1", "hyper 1.6.0", "hyper-util", @@ -2635,7 +2634,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 0.26.11", + "webpki-roots 1.0.0", ] [[package]] @@ -3509,6 +3508,12 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lyon" version = "1.0.1" @@ -4668,9 +4673,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases 0.2.1", @@ -4688,12 +4693,13 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", "getrandom 0.3.1", + "lru-slab", "rand 0.9.1", "ring", "rustc-hash 2.1.1", @@ -4717,7 +4723,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4975,7 +4981,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper-rustls 0.27.6", "hyper-util", "ipnet", "js-sys", @@ -6893,9 +6899,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index 7b93bec71..61d60b690 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -7,8 +7,6 @@ use liana::miniscript::bitcoin::{ Address, Network, }; use liana_ui::{component::modal, widget::*}; -use payjoin::io::fetch_ohttp_keys; -use payjoin::{OhttpKeys, Url}; use crate::daemon::model::LabelsLoader; use crate::dir::LianaDirectory; @@ -64,12 +62,6 @@ impl Labelled for Addresses { } } -#[derive(Clone, Debug)] -struct PayjoinSpecs { - directory: Url, - ohttp_keys: OhttpKeys, -} - pub struct ReceivePanel { data_dir: LianaDirectory, wallet: Arc, @@ -82,17 +74,10 @@ pub struct ReceivePanel { modal: Modal, warning: Option, processing: bool, - payjoin_specs: PayjoinSpecs, } impl ReceivePanel { pub fn new(data_dir: LianaDirectory, wallet: Arc) -> Self { - let ohttp_relay = Url::parse("https://pj.bobspacebkk.com").unwrap(); - let directory = Url::parse("https://payjo.in").unwrap(); - let rt = tokio::runtime::Runtime::new().unwrap(); - let ohttp_keys = rt - .block_on(async { fetch_ohttp_keys(ohttp_relay.clone(), directory.clone()).await }) - .unwrap(); Self { data_dir, wallet, @@ -105,10 +90,6 @@ impl ReceivePanel { modal: Modal::None, warning: None, processing: false, - payjoin_specs: PayjoinSpecs { - directory, - ohttp_keys, - }, } } @@ -320,11 +301,10 @@ impl State for ReceivePanel { } Message::View(view::Message::PayjoinInitiate) => { let daemon = daemon.clone(); - let payjoin_specs = self.payjoin_specs.clone(); Task::perform( async move { daemon - .receive_payjoin(payjoin_specs.directory, payjoin_specs.ohttp_keys) + .receive_payjoin() .await .map(|res| (res.address, res.derivation_index, res.payjoin_uri)) .map_err(|e| e.into()) diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index 8ec7cc293..ab9cbd459 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -37,10 +37,13 @@ use super::message::Message; fn address_card<'a>( row_index: usize, address: &'a bitcoin::Address, + maybe_payjoin_uri: Option<&String>, labels: &'a HashMap, labels_editing: &'a HashMap>, ) -> Container<'a, Message> { let addr = address.to_string(); + let payjoin_uri = maybe_payjoin_uri.unwrap_or(&String::new()).clone(); + let has_payjoin_uri = !payjoin_uri.is_empty(); card::simple( Column::new() .push(if let Some(label) = labels_editing.get(&addr) { @@ -74,6 +77,36 @@ fn address_card<'a>( ) .align_y(Alignment::Center), ) + .push_maybe(has_payjoin_uri.then_some({ + Row::new() + .push( + Container::new( + scrollable( + Column::new() + .push(Space::with_height(Length::Fixed(10.0))) + .push( + p2_regular(&payjoin_uri) + .small() + .style(theme::text::secondary), + ) + // Space between the URI and the scrollbar + .push(Space::with_height(Length::Fixed(10.0))), + ) + .direction( + scrollable::Direction::Horizontal( + scrollable::Scrollbar::new().width(2).scroller_width(2), + ), + ), + ) + .width(Length::Fill), + ) + .push( + Button::new(icon::clipboard_icon().style(theme::text::secondary)) + .on_press(Message::Clipboard(payjoin_uri.clone())) + .style(theme::button::transparent_border), + ) + .align_y(Alignment::Center) + })) .push( Row::new() .push( @@ -134,8 +167,16 @@ pub fn receive<'a>( // iterate starting from most recently generated Column::new().spacing(10).width(Length::Fill), |col, (i, address)| { + let addr = address.to_string(); + let maybe_payjoin_uri = payjoin_uris.get(&addr); addresses_count += 1; - col.push(address_card(i, address, labels, labels_editing)) + col.push(address_card( + i, + address, + maybe_payjoin_uri, + labels, + labels_editing, + )) }, )), ) @@ -168,15 +209,15 @@ pub fn receive<'a>( // prev addresses are already ordered in descending order Column::new().spacing(10).width(Length::Fill), |col, (i, address)| { + let addr = address.to_string(); + let maybe_payjoin_uri = payjoin_uris.get(&addr); col.push(if !selected.contains(address) { Button::new( Row::new() .spacing(10) .push( { - let addr = address.to_string(); let addr_len = addr.chars().count(); - let payjoin_uri = payjoin_uris.get(&addr).unwrap(); Container::new( p2_regular(if addr_len > 2 * NUM_ADDR_CHARS { format!( @@ -229,45 +270,6 @@ pub fn receive<'a>( ) .align_y(Alignment::Center), ) - .push(if !payjoin_uri.is_empty() { - Row::new() - .push( - Container::new( - scrollable( - Column::new() - .push(Space::with_height( - Length::Fixed(10.0), - )) - .push( - p2_regular(&payjoin_uri) - .small() - .style(theme::text::secondary), - ) - // Space between the URI and the scrollbar - .push(Space::with_height( - Length::Fixed(10.0), - )), - ) - .direction(scrollable::Direction::Horizontal( - scrollable::Scrollbar::new() - .width(2) - .scroller_width(2), - )), - ) - .width(Length::Fill), - ) - .push( - Button::new( - icon::clipboard_icon() - .style(theme::text::secondary), - ) - .on_press(Message::Clipboard(payjoin_uri.clone())) - .style(theme::button::transparent_border), - ) - .align_y(Alignment::Center) - } else { - Row::new() - }) .on_press(Message::SelectAddress(address.clone())) .style(theme::button::secondary) } else { @@ -275,6 +277,7 @@ pub fn receive<'a>( Button::new(address_card( addresses_count + i, address, + maybe_payjoin_uri, prev_labels, labels_editing, )) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index f4a34ea2d..de70d1aa5 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -6,7 +6,6 @@ use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; use lianad::payjoin::types::PayjoinInfo; -use payjoin::{OhttpKeys, Url}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -109,11 +108,7 @@ impl Daemon for Lianad { ) } - async fn receive_payjoin( - &self, - _directory: Url, - _ohttp_keys: OhttpKeys, - ) -> Result { + async fn receive_payjoin(&self) -> Result { unimplemented!() } diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 47eebaf9c..3d3f06463 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,7 +1,6 @@ use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; use lianad::payjoin::types::PayjoinInfo; -use payjoin::{OhttpKeys, Url}; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; @@ -122,13 +121,8 @@ impl Daemon for EmbeddedDaemon { .await } - async fn receive_payjoin( - &self, - directory: Url, - ohttp_keys: OhttpKeys, - ) -> Result { - self.command(|daemon| Ok(daemon.receive_payjoin(directory, ohttp_keys))) - .await + async fn receive_payjoin(&self) -> Result { + self.command(|daemon| Ok(daemon.receive_payjoin())).await } async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError> { diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 9275af74c..ef7c00e80 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -24,7 +24,6 @@ use lianad::{ config::Config, StartupError, }; -use payjoin::{OhttpKeys, Url}; use crate::{hw::HardwareWalletConfig, node}; @@ -116,11 +115,7 @@ pub trait Daemon: Debug { limit: usize, start_index: Option, ) -> Result; - async fn receive_payjoin( - &self, - directory: Url, - ohttp_keys: OhttpKeys, - ) -> Result; + async fn receive_payjoin(&self) -> Result; async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError>; async fn update_deriv_indexes( @@ -411,6 +406,7 @@ pub trait Daemon: Debug { /// Reimplemented by LianaLite backend async fn update_wallet_metadata( &self, + _wallet_alias: Option, _fingerprint_aliases: &HashMap, _hws: &[HardwareWalletConfig], ) -> Result<(), DaemonError> { diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index b54a828b3..f16dfc339 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -19,7 +19,6 @@ use lianad::{ config::Config, payjoin::types::PayjoinInfo, }; -use payjoin::{OhttpKeys, Url}; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; @@ -625,11 +624,7 @@ impl Daemon for BackendWalletClient { }) } - async fn receive_payjoin( - &self, - _directory: Url, - _ohttp_keys: OhttpKeys, - ) -> Result { + async fn receive_payjoin(&self) -> Result { unimplemented!() } @@ -1316,6 +1311,7 @@ fn spend_tx_from_api( desc, secp, network, + None, ); tx.load_labels(&labels); tx diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index e45c33c16..cd0dd529e 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -10,6 +10,7 @@ use crate::{ miniscript::bitcoin::absolute::LockTime, payjoin::{ db::{ReceiverPersister, SenderPersister, SessionMetadata}, + helpers::fetch_ohttp_keys, types::{PayjoinInfo, PayjoinStatus}, }, poller::PollerMessage, @@ -51,7 +52,7 @@ use payjoin::{ bitcoin::{key::Secp256k1, FeeRate, Txid}, receive::v2::{Receiver, UninitializedReceiver}, send::v2::SenderBuilder, - OhttpKeys, Uri, UriExt, Url, + Uri, UriExt, }; use serde::{Deserialize, Serialize}; @@ -372,8 +373,24 @@ impl DaemonControl { GetAddressResult::new(address, new_index, "".to_string()) } - pub fn receive_payjoin(&self, directory: Url, ohttp_keys: OhttpKeys) -> GetAddressResult { + pub fn receive_payjoin(&self) -> GetAddressResult { let mut db_conn = self.db.connection(); + + // TODO(arturgontijo): Fetch these from DB (via GUI's Settings Panel) + let ohttp_relay: &str = "https://pj.bobspacebkk.com"; + let directory = "https://payjo.in"; + + let ohttp_keys = if let Some(entry) = db_conn.payjoin_get_ohttp_keys(ohttp_relay) { + entry.1 + } else { + let ohttp_keys = std::thread::spawn(move || fetch_ohttp_keys(ohttp_relay, directory)) + .join() + .unwrap() + .unwrap(); + db_conn.payjoin_save_ohttp_keys(ohttp_relay, ohttp_keys.clone()); + ohttp_keys + }; + let index = db_conn.receive_index(); let new_index = index .increment() @@ -389,18 +406,14 @@ impl DaemonControl { let persister = ReceiverPersister::new(Arc::new(self.db.clone())).unwrap(); let session = Receiver::::create_session( address.clone(), - directory.clone(), + directory, ohttp_keys.clone(), None, ) .save(&persister) .unwrap(); - let mut payjoin_uri = session.pj_uri(); - // HACK: hardcoded amount for now - payjoin_uri.amount = Some(bitcoin::Amount::from_sat(10_000)); - - GetAddressResult::new(address, new_index, payjoin_uri.to_string()) + GetAddressResult::new(address, new_index, session.pj_uri().to_string()) } /// Initiate a payjoin sender diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index a78ebfa43..f9777f7cf 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -23,7 +23,7 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; -use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent}; +use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent, OhttpKeys}; /// Information about the wallet. /// @@ -199,6 +199,12 @@ pub trait DatabaseConnection { /// Payjoin + /// Get the next Session Id + fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)>; + + /// Save OHttpKeys + fn payjoin_save_ohttp_keys(&mut self, ohttp_relay: &str, ohttp_keys: OhttpKeys); + /// Get the next Session Id fn payjoin_next_id(&mut self, table: &str) -> u64; @@ -475,6 +481,14 @@ impl DatabaseConnection for SqliteConn { .collect() } + fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { + self.payjoin_get_ohttp_keys(ohttp_relay) + } + + fn payjoin_save_ohttp_keys(&mut self, ohttp_relay: &str, ohttp_keys: OhttpKeys) { + self.payjoin_save_ohttp_keys(ohttp_relay, ohttp_keys) + } + fn payjoin_next_id(&mut self, table: &str) -> u64 { self.payjoin_next_id(table) } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 3520df883..e296476db 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -28,7 +28,7 @@ use crate::{ payjoin::db::{SessionId, SessionWrapper}, }; use liana::descriptors::LianaDescriptor; -use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent}; +use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent, OhttpKeys}; use std::{ cmp, @@ -966,6 +966,56 @@ impl SqliteConn { .expect("Db must not fail"); } + /// Fetch Payjoin OHttpKeys and their timestamp + pub fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { + let entries = db_query( + &mut self.conn, + "SELECT timestamp, keys FROM payjoin_ohttp_keys WHERE relay = ?1 ORDER BY timestamp DESC LIMIT 1", + rusqlite::params![ohttp_relay], + |row| { + let timestamp: u32 = row.get(0)?; + let ohttp_keys_ser: Vec = row.get(1)?; + let ohttp_keys = OhttpKeys::decode(&ohttp_keys_ser).unwrap(); + Ok((timestamp, ohttp_keys)) + }, + ) + .expect("Db must not fail"); + + // Check timestamp (7-days) + if let Some(entry) = entries.first().cloned() { + let now = curr_timestamp(); + let seven_days_ago = now.saturating_sub(7 * 24 * 60 * 60); + if entry.0 < seven_days_ago { + // Delete entry + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "DELETE FROM payjoin_ohttp_keys WHERE relay = ?1", + rusqlite::params![ohttp_relay], + )?; + Ok(()) + }) + .expect("Db must not fail"); + return None; + } else { + return Some(entry); + } + } + None + } + + /// Store new OHttpKeys with timestamp + pub fn payjoin_save_ohttp_keys(&mut self, ohttp_relay: &str, ohttp_keys: OhttpKeys) { + let ohttp_keys_ser = ohttp_keys.encode().unwrap(); + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "INSERT INTO payjoin_ohttp_keys (relay, timestamp, keys) VALUES (?1, ?2, ?3)", + rusqlite::params![ohttp_relay, curr_timestamp(), ohttp_keys_ser], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + /// Create a payjoin receiver pub fn payjoin_next_id(&mut self, table: &str) -> u64 { let count = db_query( diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 2a38ad50d..ad4fedfd7 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -123,6 +123,14 @@ CREATE TABLE labels ( value TEXT NOT NULL ); +/* Payjoin OHttpKeys */ +CREATE TABLE payjoin_ohttp_keys ( + id INTEGER PRIMARY KEY NOT NULL, + relay TEXT UNIQUE NOT NULL, + timestamp INTEGER NOT NULL, + keys BLOB NOT NULL +); + /* Payjoin senders */ CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index fcbd7882f..4ca2f2521 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -5,7 +5,8 @@ use miniscript::{ psbt::PsbtExt, }; -use payjoin::bitcoin::Amount; +use payjoin::{bitcoin::Amount, IntoUrl, OhttpKeys}; +use reqwest::{header::ACCEPT, Proxy}; pub const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; @@ -13,6 +14,36 @@ pub fn http_agent() -> reqwest::blocking::Client { reqwest::blocking::Client::new() } +pub fn fetch_ohttp_keys( + ohttp_relay: impl IntoUrl, + payjoin_directory: impl IntoUrl, +) -> Result> { + let ohttp_keys_url = payjoin_directory + .into_url()? + .join("/.well-known/ohttp-gateway")?; + let proxy = Proxy::all(ohttp_relay.into_url()?.as_str())?; + let client = reqwest::blocking::Client::builder().proxy(proxy).build()?; + let res = client + .get(ohttp_keys_url) + .header(ACCEPT, "application/ohttp-keys") + .send()?; + parse_ohttp_keys_response(res) +} + +fn parse_ohttp_keys_response( + res: reqwest::blocking::Response, +) -> Result> { + if !res.status().is_success() { + return Err(format!("UnexpectedStatusCode: {}", res.status()).into()); + } + + let body = res.bytes().unwrap().to_vec(); + match OhttpKeys::decode(&body) { + Ok(ohttp_keys) => Ok(ohttp_keys), + Err(err) => Err(format!("InvalidOhttpKeys: {}", err).into()), + } +} + pub fn post_request(req: payjoin::Request) -> Result> { let http = http_agent(); match http diff --git a/lianad/src/payjoin/mod.rs b/lianad/src/payjoin/mod.rs index 2916f0851..0761f8397 100644 --- a/lianad/src/payjoin/mod.rs +++ b/lianad/src/payjoin/mod.rs @@ -1,5 +1,5 @@ pub mod db; -mod helpers; +pub mod helpers; pub mod receiver; pub mod sender; pub mod types; diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index b57776c24..ede1533b0 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -9,7 +9,7 @@ use crate::{ DaemonControl, DaemonHandle, }; use liana::descriptors; -use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent}; +use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent, OhttpKeys}; use std::convert::TryInto; use std::{ @@ -614,6 +614,14 @@ impl DatabaseConnection for DummyDatabase { ) -> Vec<(SessionId, SessionWrapper)> { todo!() } + + fn payjoin_get_ohttp_keys(&mut self, _ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { + todo!() + } + + fn payjoin_save_ohttp_keys(&mut self, _ohttp_relay: &str, _ohttp_keys: payjoin::OhttpKeys) { + todo!() + } } pub struct DummyLiana { From 82c22870fd06d0a4794a72b1f557a97413fdf628 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Thu, 5 Jun 2025 19:39:28 -0300 Subject: [PATCH 09/72] [PDK] Better receiver code structure + clippy. --- liana-gui/src/app/state/spend/step.rs | 2 +- liana-gui/src/daemon/model.rs | 19 +- lianad/src/bitcoin/d/mod.rs | 15 + lianad/src/bitcoin/mod.rs | 17 ++ lianad/src/bitcoin/poller/looper.rs | 2 +- lianad/src/commands/mod.rs | 2 +- lianad/src/database/sqlite/mod.rs | 4 +- lianad/src/payjoin/db.rs | 9 +- lianad/src/payjoin/helpers.rs | 4 +- lianad/src/payjoin/receiver.rs | 405 +++++++++++++++----------- lianad/src/payjoin/sender.rs | 22 +- lianad/src/testutils.rs | 8 +- 12 files changed, 305 insertions(+), 204 deletions(-) diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 26317659e..31c5e2840 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -967,7 +967,7 @@ impl Step for SaveSpend { fn load(&mut self, _coins: &[Coin], _tip_height: i32, draft: &TransactionDraft) { let (psbt, warnings) = draft.generated.clone().unwrap(); - let bip21 = draft.recipients.get(0).unwrap().bip21.value.clone(); + let bip21 = draft.recipients.first().unwrap().bip21.value.clone(); let payjoin_info = if bip21.is_empty() { None } else { diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index bbd55c4f6..7794a02f4 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -205,18 +205,15 @@ impl SpendTx { // TODO(arturgontijo): We should count the sigs, just in case. if let Some(payjoin_info) = &self.payjoin_info { - match payjoin_info.sender_status { - Some(PayjoinStatus::Completed) => { - let has_sigs = self - .psbt - .inputs - .iter() - .any(|psbtin| !psbtin.partial_sigs.is_empty()); - if has_sigs { - return Some(path); - } + if let Some(PayjoinStatus::Completed) = payjoin_info.sender_status { + let has_sigs = self + .psbt + .inputs + .iter() + .any(|psbtin| !psbtin.partial_sigs.is_empty()); + if has_sigs { + return Some(path); } - _ => {} } }; diff --git a/lianad/src/bitcoin/d/mod.rs b/lianad/src/bitcoin/d/mod.rs index 887192ba5..30c8f6753 100644 --- a/lianad/src/bitcoin/d/mod.rs +++ b/lianad/src/bitcoin/d/mod.rs @@ -1230,6 +1230,21 @@ impl BitcoinD { .collect() } + /// Test whether raw transactions would be accepted by the mempool. + pub fn test_mempool_accept(&self, rawtxs: Vec) -> Vec { + let hex_txs: Json = rawtxs.into_iter().map(|tx| serde_json::json!(tx)).collect(); + self.make_node_request("testmempoolaccept", params!(hex_txs)) + .as_array() + .expect("Always returns an array") + .iter() + .map(|e| { + e.get("allowed") + .and_then(|v| v.as_bool()) + .expect("Each result must have an 'allowed' boolean") + }) + .collect() + } + /// Stop bitcoind. pub fn stop(&self) { self.make_node_request("stop", None); diff --git a/lianad/src/bitcoin/mod.rs b/lianad/src/bitcoin/mod.rs index 26e755df8..742ff51f6 100644 --- a/lianad/src/bitcoin/mod.rs +++ b/lianad/src/bitcoin/mod.rs @@ -133,6 +133,11 @@ pub trait BitcoinInterface: Send { /// /// Returns `None` if the transaction is not in the mempool. fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option; + + /// Test if given raw txs will be accepted by mempool. + /// + /// Returns `None` if the transaction is not in the mempool. + fn test_mempool_accept(&self, rawtxs: Vec) -> Vec; } impl BitcoinInterface for d::BitcoinD { @@ -402,6 +407,10 @@ impl BitcoinInterface for d::BitcoinD { fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option { self.mempool_entry(txid) } + + fn test_mempool_accept(&self, rawtxs: Vec) -> Vec { + self.test_mempool_accept(rawtxs) + } } impl BitcoinInterface for electrum::Electrum { @@ -589,6 +598,10 @@ impl BitcoinInterface for electrum::Electrum { fn tip_time(&self) -> Option { self.client().tip_time().ok() } + + fn test_mempool_accept(&self, _rawtxs: Vec) -> Vec { + todo!() + } } // FIXME: do we need to repeat the entire trait implementation? Isn't there a nicer way? @@ -694,6 +707,10 @@ impl BitcoinInterface for sync::Arc> fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option { self.lock().unwrap().mempool_entry(txid) } + + fn test_mempool_accept(&self, rawtxs: Vec) -> Vec { + self.lock().unwrap().test_mempool_accept(rawtxs) + } } // FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index de863d0fd..1c37c94b6 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -407,7 +407,7 @@ pub fn poll( updates(&mut db_conn, bit, descs, secp); rescan_check(&mut db_conn, bit, descs, secp); payjoin_sender_check(db); - payjoin_receiver_check(db, descs, secp); + payjoin_receiver_check(db, bit, descs, secp); let now: u32 = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .expect("current system time must be later than epoch") diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index cd0dd529e..bdf238e3d 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -424,7 +424,7 @@ impl DaemonControl { let uri = uri.assume_checked(); let uri = uri .check_pj_supported() - .map_err(|_| format!("URI does not support Payjoin")) + .map_err(|_| "URI does not support Payjoin".to_string()) .unwrap(); let mut psbt = psbt.clone(); diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index e296476db..eb0edb69b 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -1029,9 +1029,9 @@ impl SqliteConn { ) .expect("Db must not fail"); if let Some(count) = count.first() { - return *count; + *count } else { - return 0; + 0 } } diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index a123156b6..6d871bde4 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -3,6 +3,7 @@ use payjoin::persist::SessionPersister; use payjoin::receive::v2::ReceiverSessionEvent; use payjoin::send::v2::SenderSessionEvent; use serde::{Deserialize, Serialize}; +use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::SystemTime; @@ -61,7 +62,7 @@ pub struct ReceiverPersister { } impl ReceiverPersister { - pub fn new(db: Arc) -> Result { + pub fn new(db: Arc) -> Result> { let mut db_conn = db.connection(); let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_receivers")); let session: SessionWrapper = SessionWrapper { @@ -78,7 +79,7 @@ impl ReceiverPersister { Ok(Self { db, session_id }) } - pub fn from_id(db: Arc, id: SessionId) -> Result { + pub fn from_id(db: Arc, id: SessionId) -> Result> { Ok(Self { db, session_id: id }) } @@ -148,7 +149,7 @@ pub struct SenderPersister { } impl SenderPersister { - pub fn new(db: Arc) -> Result { + pub fn new(db: Arc) -> Result> { let mut db_conn = db.connection(); let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_senders")); let session: SessionWrapper = SessionWrapper { @@ -165,7 +166,7 @@ impl SenderPersister { Ok(Self { db, session_id }) } - pub fn from_id(db: Arc, id: SessionId) -> Result { + pub fn from_id(db: Arc, id: SessionId) -> Result> { Ok(Self { db, session_id: id }) } diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 4ca2f2521..f52ec52a8 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -63,7 +63,7 @@ pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 log::info!("[Payjoin] Finalizing input at: {}", index), Err(e) => log::warn!("[Payjoin] Failed to finalize input at: {} | {}", index, e), } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 85066f7ec..4cad52410 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -7,18 +7,22 @@ use std::{ use liana::descriptors; use payjoin::{ - bitcoin::{psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, TxIn}, + bitcoin::{ + consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, TxIn, + }, persist::OptionalTransitionOutcome, receive::{ v2::{ - replay_receiver_event_log, Receiver, ReceiverSessionEvent, ReceiverState, - ReceiverWithContext, UncheckedProposal, + replay_receiver_event_log, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, + PayjoinProposal, ProvisionalProposal, Receiver, ReceiverState, ReceiverWithContext, + SessionHistory, UncheckedProposal, WantsInputs, WantsOutputs, }, InputPair, }, }; use crate::{ + bitcoin::BitcoinInterface, database::{Coin, CoinStatus, DatabaseConnection, DatabaseInterface}, payjoin::{ db::SessionMetadata, @@ -26,15 +30,119 @@ use crate::{ }, }; -use super::{ - db::{ReceiverPersister, SessionWrapper}, - types::PayjoinStatus, -}; +use super::{db::ReceiverPersister, types::PayjoinStatus}; -fn handle_directory_proposal( +fn read_from_directory( + receiver: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, + bit: &mut sync::Arc>, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + let mut receiver = receiver; + let (req, context) = receiver + .extract_req(OHTTP_RELAY) + .expect("Failed to extract request"); + let proposal = match post_request(req.clone()) { + Ok(ohttp_response) => { + let state_transition = receiver + .process_res( + ohttp_response + .bytes() + .expect("Failed to read response") + .as_ref(), + context, + ) + .save(persister); + match state_transition { + Ok(OptionalTransitionOutcome::Progress(next_state)) => next_state, + Ok(OptionalTransitionOutcome::Stasis(_current_state)) => { + return Err("NoResults".into()) + } + Err(e) => return Err(e.into()), + } + } + Err(e) => return Err(e), + }; + check_proposal(proposal, persister, db_conn, bit, descs, secp) +} + +fn check_proposal( proposal: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, + bit: &mut sync::Arc>, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + // Receive Check 1: Can Broadcast + let proposal = proposal + .check_broadcast_suitability(None, |tx| { + let result = bit.test_mempool_accept(vec![serialize_hex(tx)]); + match result.first().cloned() { + Some(can_broadcast) => Ok(can_broadcast), + None => Ok(false), + } + }) + .save(persister)?; + check_inputs_not_owned(proposal, persister, db_conn, descs, secp) +} + +fn check_inputs_not_owned( + proposal: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + let proposal = proposal + .check_inputs_not_owned(|_| Ok(false)) + .save(persister)?; + check_no_inputs_seen_before(proposal, persister, db_conn, descs, secp) +} + +fn check_no_inputs_seen_before( + proposal: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + let proposal = proposal + .check_no_inputs_seen_before(|_| Ok(false)) + .save(persister)?; + identify_receiver_outputs(proposal, persister, db_conn, descs, secp) +} + +fn identify_receiver_outputs( + proposal: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + let proposal = proposal + .identify_receiver_outputs(|_| Ok(true)) + .save(persister)?; + commit_outputs(proposal, persister, db_conn, descs, secp) +} + +fn commit_outputs( + proposal: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + let proposal = proposal.commit_outputs().save(persister)?; + contribute_inputs(proposal, persister, db_conn, descs, secp) +} + +fn contribute_inputs( + proposal: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { @@ -56,7 +164,7 @@ fn handle_directory_proposal( }; let txin = TxIn { - previous_output: outpoint.clone(), + previous_output: *outpoint, sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, ..Default::default() }; @@ -69,138 +177,127 @@ fn handle_directory_proposal( receiver_derived_desc.update_psbt_in(&mut psbtin); - candidate_inputs_map.insert(outpoint.clone(), (*coin, txin, psbtin)); + candidate_inputs_map.insert(*outpoint, (*coin, txin, psbtin)); } let candidate_inputs = candidate_inputs_map .values() .map(|(_, txin, psbtin)| InputPair::new(txin.clone(), psbtin.clone()).unwrap()); - // in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx - let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast(); + let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); - // Receive Check 1: Can Broadcast - let proposal = proposal - .check_broadcast_suitability(None, |_| Ok(true)) - .save(persister)?; - - // Receive Check 2: receiver can't sign for proposal inputs - let proposal = proposal - .check_inputs_not_owned(|_input| Ok(false)) - .save(persister)?; - - // Receive Check 3: have we seen this input before? More of a check for non-interactive i.e. payment processor receivers. - let proposal = proposal - .check_no_inputs_seen_before(|_| Ok(false)) + proposal + .contribute_inputs(vec![selected_input])? + .commit_inputs() .save(persister)?; - // Receive Check 4: identify receiver outputs - let proposal = proposal - .identify_receiver_outputs(|_| Ok(true)) - .save(persister)?; + let (_, history) = replay_receiver_event_log(persister.clone()) + .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) + .unwrap(); - // Receive Check 5: commit outputs - let proposal = proposal.commit_outputs().save(persister)?; + let psbt = history.psbt_with_contributed_inputs().unwrap(); + let bip21 = history.pj_uri().unwrap().to_string(); - let selected_input = proposal.try_preserving_privacy(candidate_inputs)?; + db_conn.store_spend(&psbt); + log::info!("[Payjoin] PSBT in the DB..."); - proposal - .contribute_inputs(vec![selected_input]) - .map_err(|e| format!("Failed to contribute inputs: {e:?}"))? - .commit_inputs() - .save(persister)?; + persister.update_metada( + Some(PayjoinStatus::Signing), + Some(psbt.unsigned_tx.compute_txid()), + Some(psbt.clone()), + Some(bip21.clone()), + ); Ok(()) } -fn poll_fallback( - receiver: Receiver, +fn finalize_proposal( + proposal: Receiver, persister: &ReceiverPersister, -) -> Result, Box> { - let mut receiver = receiver; - let (req, context) = receiver - .extract_req(OHTTP_RELAY) - .expect("Failed to extract request"); - match post_request(req.clone()) { - Ok(ohttp_response) => { - let state_transition = receiver - .process_res( - ohttp_response - .bytes() - .expect("Failed to read response") - .as_ref(), - context, - ) - .save(persister); - match state_transition { - Ok(OptionalTransitionOutcome::Progress(next_state)) => Ok(next_state), - Ok(OptionalTransitionOutcome::Stasis(_current_state)) => Err("NoResults".into()), - Err(e) => return Err(e.into()), + history: SessionHistory, + db_conn: &mut Box, + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + if let Some(txid) = history.proposal_txid() { + if let Some(psbt) = db_conn.spend_tx(&txid) { + let mut is_signed = false; + for psbtin in &psbt.inputs { + if !psbtin.partial_sigs.is_empty() { + log::debug!("[Payjoin] PSBT is signed!"); + is_signed = true; + break; + } + } + + if is_signed { + let mut psbt = psbt.clone(); + finalize_psbt(&mut psbt, secp); + + let proposal = proposal + .finalize_proposal( + |_| Ok(psbt.clone()), + None, + Some(FeeRate::from_sat_per_vb(150).unwrap()), + ) + .save(persister)?; + + send_payjoin_proposal(proposal, persister, history)?; } } - Err(e) => Err(e.into()), } + Ok(()) } -fn process_receiver_session( - session: SessionWrapper, - state: ReceiverState, - persister: ReceiverPersister, - db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], - secp: &secp256k1::Secp256k1, +fn send_payjoin_proposal( + mut proposal: Receiver, + persister: &ReceiverPersister, + history: SessionHistory, ) -> Result<(), Box> { - match state { - ReceiverState::WithContext(receiver) => { - log::info!("[Payjoin] ReceiverState::WithContext"); - if session.metadata.status == PayjoinStatus::Pending { - match poll_fallback(receiver, &persister) { - Ok(proposal) => { - handle_directory_proposal(proposal, &persister, db_conn, descs, secp)?; - - let (_, history) = replay_receiver_event_log(persister.clone()) - .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) - .unwrap(); - - let psbt = history.psbt_with_contributed_inputs().unwrap(); - let bip21 = history.pj_uri().unwrap().to_string(); - - db_conn.store_spend(&psbt); - log::info!("[Payjoin] ReceiverState::WithContext: PSBT in the DB..."); - - persister.update_metada( - Some(PayjoinStatus::Signing), - Some(psbt.unsigned_tx.compute_txid()), - Some(psbt.clone()), - Some(bip21.clone()), - ); - } - Err(_) => {} - } - } - Ok(()) + let (req, ctx) = proposal + .extract_req(OHTTP_RELAY) + .expect("Failed to extract request"); + + let psbt = proposal.psbt().clone(); + let txid = psbt.unsigned_tx.compute_txid(); + + // Respond to sender + log::info!("[Payjoin] Receiver responding to sender..."); + match post_request(req) { + Ok(resp) => { + proposal + .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) + .save(persister)?; + + let bip21 = history.pj_uri().unwrap(); + persister.update_metada( + Some(PayjoinStatus::Completed), + Some(txid), + Some(psbt), + Some(bip21.to_string()), + ); } - _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), + Err(err) => log::error!("[Payjoin] send_payjoin_proposal(): {}", err), } + Ok(()) } -pub fn payjoin_receiver_check( +fn process_receiver_session( db: &sync::Arc>, + bit: &mut sync::Arc>, descs: &[descriptors::SinglePathLianaDesc], secp: &secp256k1::Secp256k1, -) { +) -> Result<(), Box> { let mut db_conn = db.connection(); for (session_id, session) in db_conn.payjoin_get_all_receiver_sessions() { let SessionMetadata { status, - maybe_txid, maybe_bip21, .. } = session.metadata.clone(); // No need to check Completed if status == PayjoinStatus::Completed { - continue; + return Ok(()); } log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); @@ -208,76 +305,52 @@ pub fn payjoin_receiver_check( let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); - let (state, _) = replay_receiver_event_log(persister.clone()) + let (state, history) = replay_receiver_event_log(persister.clone()) .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) .unwrap(); - match status { - PayjoinStatus::Pending => { - match process_receiver_session(session, state, persister, &mut db_conn, descs, secp) - { - Ok(_) => (), - Err(e) => log::warn!("receiver_check(): {}", e), - } + match state { + ReceiverState::WithContext(context) => { + read_from_directory(context, &persister, &mut db_conn, bit, descs, secp)?; } - PayjoinStatus::Signing => { - if let Some(txid) = maybe_txid { - match db_conn.spend_tx(&txid) { - Some(psbt) => { - let mut is_signed = false; - for psbtin in &psbt.inputs { - if !psbtin.partial_sigs.is_empty() { - log::debug!("[Payjoin] PSBT is signed!"); - is_signed = true; - break; - } - } - - if is_signed { - match state { - ReceiverState::ProvisionalProposal(proposal) => { - let mut psbt = psbt.clone(); - finalize_psbt(&mut psbt, secp); - - let mut proposal = proposal - .finalize_proposal( - |_| Ok(psbt.clone()), - None, - Some(FeeRate::from_sat_per_vb(150).unwrap()), - ) - .save(&persister) - .unwrap(); - - let (req, ctx) = proposal - .extract_req(OHTTP_RELAY) - .expect("Failed to extract request"); - - // Respond to sender - log::info!("[Payjoin] Receiver responding to sender..."); - match post_request(req.clone()) { - Ok(resp) => { - proposal - .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) - .save(&persister) - .unwrap(); - - persister.update_metada(Some(PayjoinStatus::Completed), maybe_txid, Some(psbt), maybe_bip21); - }, - Err(err) => log::error!( - "[Payjoin] payjoin_receiver_check(respond_to_sender): {} -> {}", - req.url, err - ), - } - } - _ => {} - } - } - } - None => {} - } - } + ReceiverState::UncheckedProposal(proposal) => { + check_proposal(proposal, &persister, &mut db_conn, bit, descs, secp)?; + } + ReceiverState::MaybeInputsOwned(proposal) => { + check_inputs_not_owned(proposal, &persister, &mut db_conn, descs, secp)?; + } + ReceiverState::MaybeInputsSeen(proposal) => { + check_no_inputs_seen_before(proposal, &persister, &mut db_conn, descs, secp)?; + } + ReceiverState::OutputsUnknown(proposal) => { + identify_receiver_outputs(proposal, &persister, &mut db_conn, descs, secp)?; + } + ReceiverState::WantsOutputs(proposal) => { + commit_outputs(proposal, &persister, &mut db_conn, descs, secp)?; + } + ReceiverState::WantsInputs(proposal) => { + contribute_inputs(proposal, &persister, &mut db_conn, descs, secp)? + } + ReceiverState::ProvisionalProposal(proposal) => { + finalize_proposal(proposal, &persister, history, &mut db_conn, secp)? + } + ReceiverState::PayjoinProposal(proposal) => { + send_payjoin_proposal(proposal, &persister, history)? } - _ => {} + _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), } } + Ok(()) +} + +pub fn payjoin_receiver_check( + db: &sync::Arc>, + bit: &mut sync::Arc>, + descs: &[descriptors::SinglePathLianaDesc], + secp: &secp256k1::Secp256k1, +) { + match process_receiver_session(db, bit, descs, secp) { + Ok(_) => (), + Err(e) => log::warn!("process_receiver_session(): {}", e), + } } diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 2c047a894..a6c9cc49c 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -24,10 +24,7 @@ fn get_proposed_payjoin_psbt( match post_request(req) { Ok(resp) => { let res = context - .process_response( - &resp.bytes().expect("Failed to read response").as_ref(), - ctx, - ) + .process_response(resp.bytes().expect("Failed to read response").as_ref(), ctx) .save(persister); match res { Ok(OptionalTransitionOutcome::Progress(proposal)) => { @@ -44,7 +41,7 @@ fn get_proposed_payjoin_psbt( } } } - Err(e) => Err(e.into()), + Err(e) => Err(e), } } @@ -57,14 +54,11 @@ fn post_orginal_proposal( Ok(resp) => { log::info!("[Payjoin] Posted original proposal..."); sender - .process_response( - &resp.bytes().expect("Failed to read response").as_ref(), - ctx, - ) + .process_response(resp.bytes().expect("Failed to read response").as_ref(), ctx) .save(persister)?; Ok(()) } - Err(e) => Err(e.into()), + Err(e) => Err(e), } } @@ -79,7 +73,7 @@ fn process_sender_session( Ok(_) => {} Err(err) => log::warn!("post_orginal_proposal(): {}", err), } - return Ok(None); + Ok(None) } SenderState::V2GetContext(context) => { log::info!("[Payjoin] SenderState::V2GetContext"); @@ -91,9 +85,9 @@ fn process_sender_session( "[Payjoin] SenderState::ProposalReceived: {}", psbt.to_string() ); - return Ok(Some(psbt.clone())); + Ok(Some(psbt.clone())) } - _ => return Err(format!("Unexpected sender state").into()), + _ => Err("Unexpected sender state".into()), } } @@ -141,7 +135,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) let mut input_without_sigs = psbt.inputs[index].clone(); input_without_sigs.partial_sigs = Default::default(); input_fields_to_restore - .insert(txin.previous_output.clone(), input_without_sigs); + .insert(txin.previous_output, input_without_sigs); } log::info!( "[Payjoin] Deleting original Payjoin psbt (txid={txid})" diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index ede1533b0..1c61fab3d 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -145,6 +145,10 @@ impl BitcoinInterface for DummyBitcoind { fn mempool_entry(&self, _: &bitcoin::Txid) -> Option { None } + + fn test_mempool_accept(&self, _rawtxs: Vec) -> Vec { + todo!() + } } struct DummyDbState { @@ -614,11 +618,11 @@ impl DatabaseConnection for DummyDatabase { ) -> Vec<(SessionId, SessionWrapper)> { todo!() } - + fn payjoin_get_ohttp_keys(&mut self, _ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { todo!() } - + fn payjoin_save_ohttp_keys(&mut self, _ohttp_relay: &str, _ohttp_keys: payjoin::OhttpKeys) { todo!() } From 7d69acccb39a489038d0e8a58c49f8f5434ce2ea Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Thu, 5 Jun 2025 19:58:36 -0300 Subject: [PATCH 10/72] [PDK] fix --- lianad/src/payjoin/receiver.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 4cad52410..93306d4e1 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -297,7 +297,7 @@ fn process_receiver_session( // No need to check Completed if status == PayjoinStatus::Completed { - return Ok(()); + continue; } log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); From b275f7b5414551a9cbb7c2fcaaf0b9defa800562 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Wed, 11 Jun 2025 18:36:18 -0300 Subject: [PATCH 11/72] Code review --- liana-gui/Cargo.toml | 4 ++-- liana-gui/src/app/state/spend/step.rs | 19 ++++++++++--------- lianad/Cargo.toml | 4 ++-- lianad/src/commands/mod.rs | 2 +- lianad/src/database/sqlite/mod.rs | 16 ++++++++-------- lianad/src/database/sqlite/schema.rs | 4 ++-- lianad/src/payjoin/db.rs | 4 ++-- lianad/src/payjoin/receiver.rs | 4 ++-- lianad/src/payjoin/sender.rs | 4 ++-- 9 files changed, 31 insertions(+), 30 deletions(-) diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index eded0eeaf..720c21a3a 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,8 +55,8 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} -payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"]} +# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"] } +payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"] } [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 31c5e2840..729b27da6 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -604,15 +604,16 @@ impl Step for DefineSpend { } view::CreateSpendMessage::Bip21Edited(i, bip21) => { - self.recipients.get_mut(i).unwrap().bip21.value = bip21.clone(); - if let Ok(uri) = Uri::try_from(bip21.as_str()) { - if let Ok(address) = uri.address.require_network(cache.network) { - self.recipients.get_mut(i).unwrap().address.value = - address.to_string(); - } - if let Some(amount) = uri.amount { - self.recipients.get_mut(i).unwrap().amount.value = - amount.to_string_in(Denomination::Bitcoin); + if let Some(recipient) = self.recipients.get_mut(i) { + recipient.bip21.value = bip21.clone(); + if let Ok(uri) = Uri::try_from(bip21.as_str()) { + if let Ok(address) = uri.address.require_network(cache.network) { + recipient.address.value = address.to_string(); + } + if let Some(amount) = uri.amount { + recipient.amount.value = + amount.to_string_in(Denomination::Bitcoin); + } } } } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index df2e0a726..1eef8b9c9 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,6 +60,6 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"]} -payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"]} +# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"] } +payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"] } reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index bdf238e3d..b64f3d137 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -436,7 +436,7 @@ impl DaemonControl { .save(&persister) .unwrap(); - persister.update_metada( + persister.update_metadata( Some(PayjoinStatus::Pending), Some(psbt.unsigned_tx.compute_txid()), Some(psbt.clone()), diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index eb0edb69b..4265ff4b2 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -967,11 +967,11 @@ impl SqliteConn { } /// Fetch Payjoin OHttpKeys and their timestamp - pub fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { + pub fn payjoin_get_ohttp_keys(&mut self, relay_url: &str) -> Option<(u32, OhttpKeys)> { let entries = db_query( &mut self.conn, - "SELECT timestamp, keys FROM payjoin_ohttp_keys WHERE relay = ?1 ORDER BY timestamp DESC LIMIT 1", - rusqlite::params![ohttp_relay], + "SELECT timestamp, key FROM payjoin_ohttp_keys WHERE relay_url = ?1 ORDER BY timestamp DESC LIMIT 1", + rusqlite::params![relay_url], |row| { let timestamp: u32 = row.get(0)?; let ohttp_keys_ser: Vec = row.get(1)?; @@ -989,8 +989,8 @@ impl SqliteConn { // Delete entry db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "DELETE FROM payjoin_ohttp_keys WHERE relay = ?1", - rusqlite::params![ohttp_relay], + "DELETE FROM payjoin_ohttp_keys WHERE relay_url = ?1", + rusqlite::params![relay_url], )?; Ok(()) }) @@ -1004,12 +1004,12 @@ impl SqliteConn { } /// Store new OHttpKeys with timestamp - pub fn payjoin_save_ohttp_keys(&mut self, ohttp_relay: &str, ohttp_keys: OhttpKeys) { + pub fn payjoin_save_ohttp_keys(&mut self, relay_url: &str, ohttp_keys: OhttpKeys) { let ohttp_keys_ser = ohttp_keys.encode().unwrap(); db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_ohttp_keys (relay, timestamp, keys) VALUES (?1, ?2, ?3)", - rusqlite::params![ohttp_relay, curr_timestamp(), ohttp_keys_ser], + "INSERT INTO payjoin_ohttp_keys (relay_url, timestamp, key) VALUES (?1, ?2, ?3)", + rusqlite::params![relay_url, curr_timestamp(), ohttp_keys_ser], )?; Ok(()) }) diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index ad4fedfd7..0da66aea4 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -126,9 +126,9 @@ CREATE TABLE labels ( /* Payjoin OHttpKeys */ CREATE TABLE payjoin_ohttp_keys ( id INTEGER PRIMARY KEY NOT NULL, - relay TEXT UNIQUE NOT NULL, + relay_url TEXT UNIQUE NOT NULL, timestamp INTEGER NOT NULL, - keys BLOB NOT NULL + key BLOB NOT NULL ); /* Payjoin senders */ diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 6d871bde4..857f30634 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -83,7 +83,7 @@ impl ReceiverPersister { Ok(Self { db, session_id: id }) } - pub fn update_metada( + pub fn update_metadata( &self, status: Option, maybe_txid: Option, @@ -170,7 +170,7 @@ impl SenderPersister { Ok(Self { db, session_id: id }) } - pub fn update_metada( + pub fn update_metadata( &self, status: Option, maybe_txid: Option, diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 93306d4e1..fd40b5a6f 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -201,7 +201,7 @@ fn contribute_inputs( db_conn.store_spend(&psbt); log::info!("[Payjoin] PSBT in the DB..."); - persister.update_metada( + persister.update_metadata( Some(PayjoinStatus::Signing), Some(psbt.unsigned_tx.compute_txid()), Some(psbt.clone()), @@ -269,7 +269,7 @@ fn send_payjoin_proposal( .save(persister)?; let bip21 = history.pj_uri().unwrap(); - persister.update_metada( + persister.update_metadata( Some(PayjoinStatus::Completed), Some(txid), Some(psbt), diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index a6c9cc49c..33b3fa790 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -116,7 +116,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) match status { PayjoinStatus::Pending => match process_sender_session(state, &persister) { - Ok(_) => persister.update_metada( + Ok(_) => persister.update_metadata( Some(PayjoinStatus::WaitingReceiver), maybe_txid, maybe_psbt, @@ -159,7 +159,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) ); db_conn.store_spend(&new_psbt); - persister.update_metada( + persister.update_metadata( Some(PayjoinStatus::Completed), Some(new_txid), Some(new_psbt), From df4d285e92a8d4890270066d5f6c456dc6e0485f Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Sun, 15 Jun 2025 11:44:09 -0300 Subject: [PATCH 12/72] [PDK] Use SEL remote branch + contribute_inputs_with_weights() --- Cargo.lock | 1 + liana-gui/Cargo.toml | 3 +- lianad/Cargo.toml | 3 +- lianad/src/bitcoin/poller/looper.rs | 30 ++++++++------ lianad/src/bitcoin/poller/mod.rs | 13 ++---- lianad/src/payjoin/receiver.rs | 63 ++++++++++++++++------------- 6 files changed, 58 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5935c2bd0..e9f6703dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,6 +4285,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.23.0" +source = "git+https://github.com/arturgontijo/rust-payjoin.git?branch=session-event-log-sketch#ca37d0fe7c1d9d7a3d24f4b5e34183bc13232b64" dependencies = [ "bhttp", "bitcoin", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 720c21a3a..f304a2f4e 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,8 +55,7 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"] } -payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "session-event-log-sketch", features = ["v2", "io"] } [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 1eef8b9c9..34182f945 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,6 +60,5 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -# payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "liana-poc", features = ["v2", "io"] } -payjoin = { path = "../../../armin/rust-payjoin/payjoin", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "session-event-log-sketch", features = ["v2", "io"] } reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/bitcoin/poller/looper.rs b/lianad/src/bitcoin/poller/looper.rs index 1c37c94b6..938f99bde 100644 --- a/lianad/src/bitcoin/poller/looper.rs +++ b/lianad/src/bitcoin/poller/looper.rs @@ -29,7 +29,7 @@ fn update_coins( bit: &impl BitcoinInterface, db_conn: &mut Box, previous_tip: &BlockChainTip, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> UpdatedCoins { let network = db_conn.network(); @@ -37,6 +37,10 @@ fn update_coins( log::debug!("Current coins: {:?}", curr_coins); // Start by fetching newly received coins. + let descs = &[ + desc.receive_descriptor().clone(), + desc.change_descriptor().clone(), + ]; let mut received = Vec::new(); for utxo in bit.received_coins(previous_tip, descs) { let UTxO { @@ -243,7 +247,7 @@ fn new_tip(bit: &impl BitcoinInterface, current_tip: &BlockChainTip) -> TipUpdat fn updates( db_conn: &mut Box, bit: &mut impl BitcoinInterface, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) { // Check if there was a new block before we update our state. @@ -266,7 +270,7 @@ fn updates( // between our former chain and the new one, then restart fresh. db_conn.rollback_tip(&new_tip); log::info!("Tip was rolled back to '{}'.", new_tip); - return updates(db_conn, bit, descs, secp); + return updates(db_conn, bit, desc, secp); } } } @@ -287,23 +291,23 @@ fn updates( &reorg_common_ancestor ); } - return updates(db_conn, bit, descs, secp); + return updates(db_conn, bit, desc, secp); } Err(e) => { log::error!("Error syncing wallet: '{}'.", e); thread::sleep(time::Duration::from_secs(2)); - return updates(db_conn, bit, descs, secp); + return updates(db_conn, bit, desc, secp); } }; // Then check the state of our coins. Do it even if the tip did not change since last poll, as // we may have unconfirmed transactions. - let updated_coins = update_coins(bit, db_conn, ¤t_tip, descs, secp); + let updated_coins = update_coins(bit, db_conn, ¤t_tip, desc, secp); // If the tip changed while we were polling our Bitcoin interface, start over. if bit.chain_tip() != latest_tip { log::info!("Chain tip changed while we were updating our state. Starting over."); - return updates(db_conn, bit, descs, secp); + return updates(db_conn, bit, desc, secp); } // Transactions must be added to the DB before coins due to foreign key constraints. @@ -332,7 +336,7 @@ fn updates( fn rescan_check( db_conn: &mut Box, bit: &mut impl BitcoinInterface, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) { log::debug!("Checking the state of an ongoing rescan if there is any"); @@ -370,7 +374,7 @@ fn rescan_check( "Rolling back our internal tip to '{}' to update our internal state with past transactions.", rescan_tip ); - updates(db_conn, bit, descs, secp) + updates(db_conn, bit, desc, secp) } else { log::debug!("No ongoing rescan."); } @@ -401,13 +405,13 @@ pub fn poll( bit: &mut sync::Arc>, db: &sync::Arc>, secp: &secp256k1::Secp256k1, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, ) { let mut db_conn = db.connection(); - updates(&mut db_conn, bit, descs, secp); - rescan_check(&mut db_conn, bit, descs, secp); + updates(&mut db_conn, bit, desc, secp); + rescan_check(&mut db_conn, bit, desc, secp); payjoin_sender_check(db); - payjoin_receiver_check(db, bit, descs, secp); + payjoin_receiver_check(db, bit, desc, secp); let now: u32 = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .expect("current system time must be later than epoch") diff --git a/lianad/src/bitcoin/poller/mod.rs b/lianad/src/bitcoin/poller/mod.rs index bbc11933e..f606e7758 100644 --- a/lianad/src/bitcoin/poller/mod.rs +++ b/lianad/src/bitcoin/poller/mod.rs @@ -23,8 +23,7 @@ pub struct Poller { bit: sync::Arc>, db: sync::Arc>, secp: secp256k1::Secp256k1, - // The receive and change descriptors (in this order). - descs: [descriptors::SinglePathLianaDesc; 2], + desc: descriptors::LianaDescriptor, } impl Poller { @@ -34,10 +33,6 @@ impl Poller { desc: descriptors::LianaDescriptor, ) -> Poller { let secp = secp256k1::Secp256k1::verification_only(); - let descs = [ - desc.receive_descriptor().clone(), - desc.change_descriptor().clone(), - ]; // On first startup the tip may be NULL. Make sure it's set as the poller relies on it. looper::maybe_initialize_tip(&bit, &db); @@ -46,7 +41,7 @@ impl Poller { bit, db, secp, - descs, + desc, } } @@ -108,7 +103,7 @@ impl Poller { // poll too soon. last_poll = Some(time::Instant::now()); if synced { - looper::poll(&mut self.bit, &self.db, &self.secp, &self.descs); + looper::poll(&mut self.bit, &self.db, &self.secp, &self.desc); } else { log::warn!("Skipped poll as block chain is still synchronizing."); } @@ -142,7 +137,7 @@ impl Poller { } } - looper::poll(&mut self.bit, &self.db, &self.secp, &self.descs); + looper::poll(&mut self.bit, &self.db, &self.secp, &self.desc); } } } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index fd40b5a6f..b09b8978d 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -8,7 +8,8 @@ use liana::descriptors; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, TxIn, + consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, + TxIn, Weight, }, persist::OptionalTransitionOutcome, receive::{ @@ -37,7 +38,7 @@ fn read_from_directory( persister: &ReceiverPersister, db_conn: &mut Box, bit: &mut sync::Arc>, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let mut receiver = receiver; @@ -65,7 +66,7 @@ fn read_from_directory( } Err(e) => return Err(e), }; - check_proposal(proposal, persister, db_conn, bit, descs, secp) + check_proposal(proposal, persister, db_conn, bit, desc, secp) } fn check_proposal( @@ -73,7 +74,7 @@ fn check_proposal( persister: &ReceiverPersister, db_conn: &mut Box, bit: &mut sync::Arc>, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { // Receive Check 1: Can Broadcast @@ -86,64 +87,64 @@ fn check_proposal( } }) .save(persister)?; - check_inputs_not_owned(proposal, persister, db_conn, descs, secp) + check_inputs_not_owned(proposal, persister, db_conn, desc, secp) } fn check_inputs_not_owned( proposal: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal .check_inputs_not_owned(|_| Ok(false)) .save(persister)?; - check_no_inputs_seen_before(proposal, persister, db_conn, descs, secp) + check_no_inputs_seen_before(proposal, persister, db_conn, desc, secp) } fn check_no_inputs_seen_before( proposal: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal .check_no_inputs_seen_before(|_| Ok(false)) .save(persister)?; - identify_receiver_outputs(proposal, persister, db_conn, descs, secp) + identify_receiver_outputs(proposal, persister, db_conn, desc, secp) } fn identify_receiver_outputs( proposal: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal .identify_receiver_outputs(|_| Ok(true)) .save(persister)?; - commit_outputs(proposal, persister, db_conn, descs, secp) + commit_outputs(proposal, persister, db_conn, desc, secp) } fn commit_outputs( proposal: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal.commit_outputs().save(persister)?; - contribute_inputs(proposal, persister, db_conn, descs, secp) + contribute_inputs(proposal, persister, db_conn, desc, secp) } fn contribute_inputs( proposal: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); @@ -157,10 +158,11 @@ fn contribute_inputs( let txout = tx.tx_out(outpoint.vout as usize)?.clone(); - let receiver_derived_desc = if coin.is_change { - descs[1].derive(coin.derivation_index, secp) + let derived_desc = if coin.is_change { + desc.change_descriptor().derive(coin.derivation_index, secp) } else { - descs[0].derive(coin.derivation_index, secp) + desc.receive_descriptor() + .derive(coin.derivation_index, secp) }; let txin = TxIn { @@ -175,7 +177,7 @@ fn contribute_inputs( ..Default::default() }; - receiver_derived_desc.update_psbt_in(&mut psbtin); + derived_desc.update_psbt_in(&mut psbtin); candidate_inputs_map.insert(*outpoint, (*coin, txin, psbtin)); } @@ -187,7 +189,10 @@ fn contribute_inputs( let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); proposal - .contribute_inputs(vec![selected_input])? + .contribute_inputs_with_weights( + vec![selected_input], + vec![Weight::from_wu_usize(desc.max_sat_weight(true))], + )? .commit_inputs() .save(persister)?; @@ -284,7 +289,7 @@ fn send_payjoin_proposal( fn process_receiver_session( db: &sync::Arc>, bit: &mut sync::Arc>, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let mut db_conn = db.connection(); @@ -311,25 +316,25 @@ fn process_receiver_session( match state { ReceiverState::WithContext(context) => { - read_from_directory(context, &persister, &mut db_conn, bit, descs, secp)?; + read_from_directory(context, &persister, &mut db_conn, bit, desc, secp)?; } ReceiverState::UncheckedProposal(proposal) => { - check_proposal(proposal, &persister, &mut db_conn, bit, descs, secp)?; + check_proposal(proposal, &persister, &mut db_conn, bit, desc, secp)?; } ReceiverState::MaybeInputsOwned(proposal) => { - check_inputs_not_owned(proposal, &persister, &mut db_conn, descs, secp)?; + check_inputs_not_owned(proposal, &persister, &mut db_conn, desc, secp)?; } ReceiverState::MaybeInputsSeen(proposal) => { - check_no_inputs_seen_before(proposal, &persister, &mut db_conn, descs, secp)?; + check_no_inputs_seen_before(proposal, &persister, &mut db_conn, desc, secp)?; } ReceiverState::OutputsUnknown(proposal) => { - identify_receiver_outputs(proposal, &persister, &mut db_conn, descs, secp)?; + identify_receiver_outputs(proposal, &persister, &mut db_conn, desc, secp)?; } ReceiverState::WantsOutputs(proposal) => { - commit_outputs(proposal, &persister, &mut db_conn, descs, secp)?; + commit_outputs(proposal, &persister, &mut db_conn, desc, secp)?; } ReceiverState::WantsInputs(proposal) => { - contribute_inputs(proposal, &persister, &mut db_conn, descs, secp)? + contribute_inputs(proposal, &persister, &mut db_conn, desc, secp)? } ReceiverState::ProvisionalProposal(proposal) => { finalize_proposal(proposal, &persister, history, &mut db_conn, secp)? @@ -346,10 +351,10 @@ fn process_receiver_session( pub fn payjoin_receiver_check( db: &sync::Arc>, bit: &mut sync::Arc>, - descs: &[descriptors::SinglePathLianaDesc], + desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) { - match process_receiver_session(db, bit, descs, secp) { + match process_receiver_session(db, bit, desc, secp) { Ok(_) => (), Err(e) => log::warn!("process_receiver_session(): {}", e), } From b7e55e40beb91d01a43380d2c9836899fe708f36 Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Mon, 7 Jul 2025 16:06:05 -0300 Subject: [PATCH 13/72] Using SEL with p2wsh hack. --- Cargo.lock | 10 ++++---- liana-gui/Cargo.toml | 2 +- liana/src/signer.rs | 9 +------ lianad/Cargo.toml | 2 +- lianad/src/commands/mod.rs | 15 +++++++++--- lianad/src/database/mod.rs | 2 +- lianad/src/database/sqlite/mod.rs | 2 +- lianad/src/payjoin/db.rs | 4 ++-- lianad/src/payjoin/helpers.rs | 10 ++++++-- lianad/src/payjoin/receiver.rs | 39 ++++++++++++++----------------- lianad/src/payjoin/sender.rs | 21 ++++++++--------- lianad/src/testutils.rs | 2 +- 12 files changed, 61 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9f6703dc..eafc582bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3417,7 +3417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -4284,8 +4284,8 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" -version = "0.23.0" -source = "git+https://github.com/arturgontijo/rust-payjoin.git?branch=session-event-log-sketch#ca37d0fe7c1d9d7a3d24f4b5e34183bc13232b64" +version = "0.24.0" +source = "git+https://github.com/arturgontijo/rust-payjoin.git?branch=p2wsh-fix#353dd8ab6aace31713d97d0e348c02197273a2af" dependencies = [ "bhttp", "bitcoin", @@ -4724,7 +4724,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5323,7 +5323,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.12.0", "rand 0.8.5", "secp256k1-sys", "serde", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index f304a2f4e..410c475d4 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,7 +55,7 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "session-event-log-sketch", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "p2wsh-fix", features = ["v2", "io"] } [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/liana/src/signer.rs b/liana/src/signer.rs index f87cbc765..a996a28ac 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -366,20 +366,13 @@ impl HotSigner { let master_fingerprint = self.fingerprint(secp); let mut sighash_cache = sighash::SighashCache::new(&psbt.unsigned_tx); - // TODO(arturgontijo): my_prevouts (my psbt.inputs only) let prevouts: Vec<_> = psbt .inputs .iter() .filter_map(|psbt_in| psbt_in.witness_utxo.clone()) .collect(); if prevouts.len() != psbt.inputs.len() { - // TODO(arturgontijo): Skip for now... - log::warn!( - "Not throwing SignerError::IncompletePsbt: prevouts.len({}) != psbt.inputs.len({})", - prevouts.len(), - psbt.inputs.len() - ); - // return Err(SignerError::IncompletePsbt); + return Err(SignerError::IncompletePsbt); } // Sign each input in the PSBT. diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 34182f945..2002fa663 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,5 +60,5 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "session-event-log-sketch", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "p2wsh-fix", features = ["v2", "io"] } reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index b64f3d137..26e6bf8b6 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -9,7 +9,7 @@ use crate::{ database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, payjoin::{ - db::{ReceiverPersister, SenderPersister, SessionMetadata}, + db::{ReceiverPersister, SenderPersister, SessionId, SessionMetadata, SessionWrapper}, helpers::fetch_ohttp_keys, types::{PayjoinInfo, PayjoinStatus}, }, @@ -50,7 +50,7 @@ use miniscript::{ }; use payjoin::{ bitcoin::{key::Secp256k1, FeeRate, Txid}, - receive::v2::{Receiver, UninitializedReceiver}, + receive::v2::{Receiver, SessionEvent as ReceiverSessionEvent, UninitializedReceiver}, send::v2::SenderBuilder, Uri, UriExt, }; @@ -492,6 +492,15 @@ impl DaemonControl { } } + pub fn payjoin_get_all_receiver_sessions(&self) -> Vec<(SessionId, SessionWrapper)> { + let mut db_conn = self.db.connection(); + db_conn.payjoin_get_all_receiver_sessions() + } + + pub fn payjoin_get_receiver_session(&self, session_id: &SessionId) -> Option { + ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()).ok() + } + /// Update derivation indexes pub fn update_deriv_indexes( &self, @@ -1035,7 +1044,7 @@ impl DaemonControl { for index in 0..spend_psbt.inputs.len() { match spend_psbt.finalize_inp_mut(&self.secp, index) { Ok(_) => log::info!("Finalizing input at: {}", index), - Err(_) => log::warn!("Not finalizing input at: {}", index), + Err(e) => log::warn!("Not finalizing input at: {} | {}", index, e), } } diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index f9777f7cf..ec802fab5 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -23,7 +23,7 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; -use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent, OhttpKeys}; +use payjoin::{receive::v2::SessionEvent as ReceiverSessionEvent, send::v2::SessionEvent as SenderSessionEvent, OhttpKeys}; /// Information about the wallet. /// diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 4265ff4b2..a0597bf1f 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -28,7 +28,7 @@ use crate::{ payjoin::db::{SessionId, SessionWrapper}, }; use liana::descriptors::LianaDescriptor; -use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent, OhttpKeys}; +use payjoin::{receive::v2::SessionEvent as ReceiverSessionEvent, send::v2::SessionEvent as SenderSessionEvent, OhttpKeys}; use std::{ cmp, diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 857f30634..9dd75395d 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -1,7 +1,7 @@ use payjoin::bitcoin::{Psbt, Txid}; use payjoin::persist::SessionPersister; -use payjoin::receive::v2::ReceiverSessionEvent; -use payjoin::send::v2::SenderSessionEvent; +use payjoin::receive::v2::SessionEvent as ReceiverSessionEvent; +use payjoin::send::v2::SessionEvent as SenderSessionEvent; use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt::{self, Display, Formatter}; diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index f52ec52a8..5bd9efa30 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -68,6 +68,10 @@ pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 log::info!("[Payjoin] Finalizing input at: {}", index), Err(e) => log::warn!("[Payjoin] Failed to finalize input at: {} | {}", index, e), } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index b09b8978d..753c1a668 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -9,14 +9,13 @@ use liana::descriptors; use payjoin::{ bitcoin::{ consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, - TxIn, Weight, + TxIn, }, persist::OptionalTransitionOutcome, receive::{ v2::{ - replay_receiver_event_log, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, - PayjoinProposal, ProvisionalProposal, Receiver, ReceiverState, ReceiverWithContext, - SessionHistory, UncheckedProposal, WantsInputs, WantsOutputs, + replay_event_log, Initialized, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, PayjoinProposal, ProvisionalProposal, + ReceiveSession, Receiver, SessionHistory, UncheckedProposal, WantsInputs, WantsOutputs }, InputPair, }, @@ -34,7 +33,7 @@ use crate::{ use super::{db::ReceiverPersister, types::PayjoinStatus}; fn read_from_directory( - receiver: Receiver, + receiver: Receiver, persister: &ReceiverPersister, db_conn: &mut Box, bit: &mut sync::Arc>, @@ -189,14 +188,11 @@ fn contribute_inputs( let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); proposal - .contribute_inputs_with_weights( - vec![selected_input], - vec![Weight::from_wu_usize(desc.max_sat_weight(true))], - )? + .contribute_inputs(vec![selected_input])? .commit_inputs() .save(persister)?; - let (_, history) = replay_receiver_event_log(persister.clone()) + let (_, history) = replay_event_log(persister) .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) .unwrap(); @@ -223,7 +219,8 @@ fn finalize_proposal( db_conn: &mut Box, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { - if let Some(txid) = history.proposal_txid() { + if let Some(proposed_psbt) = history.psbt_with_contributed_inputs() { + let txid = proposed_psbt.unsigned_tx.compute_txid(); if let Some(psbt) = db_conn.spend_tx(&txid) { let mut is_signed = false; for psbtin in &psbt.inputs { @@ -310,36 +307,36 @@ fn process_receiver_session( let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); - let (state, history) = replay_receiver_event_log(persister.clone()) + let (state, history) = replay_event_log(&persister) .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) .unwrap(); match state { - ReceiverState::WithContext(context) => { + ReceiveSession::Initialized(context) => { read_from_directory(context, &persister, &mut db_conn, bit, desc, secp)?; } - ReceiverState::UncheckedProposal(proposal) => { + ReceiveSession::UncheckedProposal(proposal) => { check_proposal(proposal, &persister, &mut db_conn, bit, desc, secp)?; } - ReceiverState::MaybeInputsOwned(proposal) => { + ReceiveSession::MaybeInputsOwned(proposal) => { check_inputs_not_owned(proposal, &persister, &mut db_conn, desc, secp)?; } - ReceiverState::MaybeInputsSeen(proposal) => { + ReceiveSession::MaybeInputsSeen(proposal) => { check_no_inputs_seen_before(proposal, &persister, &mut db_conn, desc, secp)?; } - ReceiverState::OutputsUnknown(proposal) => { + ReceiveSession::OutputsUnknown(proposal) => { identify_receiver_outputs(proposal, &persister, &mut db_conn, desc, secp)?; } - ReceiverState::WantsOutputs(proposal) => { + ReceiveSession::WantsOutputs(proposal) => { commit_outputs(proposal, &persister, &mut db_conn, desc, secp)?; } - ReceiverState::WantsInputs(proposal) => { + ReceiveSession::WantsInputs(proposal) => { contribute_inputs(proposal, &persister, &mut db_conn, desc, secp)? } - ReceiverState::ProvisionalProposal(proposal) => { + ReceiveSession::ProvisionalProposal(proposal) => { finalize_proposal(proposal, &persister, history, &mut db_conn, secp)? } - ReceiverState::PayjoinProposal(proposal) => { + ReceiveSession::PayjoinProposal(proposal) => { send_payjoin_proposal(proposal, &persister, history)? } _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 33b3fa790..93790960b 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -9,8 +9,8 @@ use std::sync::{self, Arc}; use payjoin::bitcoin::Psbt; use payjoin::persist::OptionalTransitionOutcome; -use payjoin::send::v2::Sender; -use payjoin::send::v2::{replay_sender_event_log, SenderState, SenderWithReplyKey, V2GetContext}; +use payjoin::send::v2::{Sender, WithReplyKey}; +use payjoin::send::v2::{replay_event_log, SendSession, V2GetContext}; use super::db::SenderPersister; use super::helpers::OHTTP_RELAY; @@ -27,9 +27,9 @@ fn get_proposed_payjoin_psbt( .process_response(resp.bytes().expect("Failed to read response").as_ref(), ctx) .save(persister); match res { - Ok(OptionalTransitionOutcome::Progress(proposal)) => { + Ok(OptionalTransitionOutcome::Progress(psbt)) => { log::info!("[Payjoin] ProposalReceived!"); - Ok(Some(proposal.psbt().clone())) + Ok(Some(psbt)) } Ok(OptionalTransitionOutcome::Stasis(_current_state)) => { log::info!("[Payjoin] No response yet."); @@ -46,7 +46,7 @@ fn get_proposed_payjoin_psbt( } fn post_orginal_proposal( - sender: Sender, + sender: Sender, persister: &SenderPersister, ) -> Result<(), Box> { let (req, ctx) = sender.extract_v2(OHTTP_RELAY)?; @@ -63,11 +63,11 @@ fn post_orginal_proposal( } fn process_sender_session( - state: SenderState, + state: SendSession, persister: &SenderPersister, ) -> Result, Box> { match state { - SenderState::WithReplyKey(sender) => { + SendSession::WithReplyKey(sender) => { log::info!("[Payjoin] SenderState::WithReplyKey"); match post_orginal_proposal(sender, persister) { Ok(_) => {} @@ -75,12 +75,11 @@ fn process_sender_session( } Ok(None) } - SenderState::V2GetContext(context) => { + SendSession::V2GetContext(context) => { log::info!("[Payjoin] SenderState::V2GetContext"); get_proposed_payjoin_psbt(context, persister) } - SenderState::ProposalReceived(proposal) => { - let psbt = proposal.psbt(); + SendSession::ProposalReceived(psbt) => { log::info!( "[Payjoin] SenderState::ProposalReceived: {}", psbt.to_string() @@ -110,7 +109,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); - let (state, _) = replay_sender_event_log(persister.clone()) + let (state, _) = replay_event_log(&persister) .map_err(|e| format!("Failed to replay sender event log: {:?}", e)) .unwrap(); diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 1c61fab3d..717d3c7ad 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -9,7 +9,7 @@ use crate::{ DaemonControl, DaemonHandle, }; use liana::descriptors; -use payjoin::{receive::v2::ReceiverSessionEvent, send::v2::SenderSessionEvent, OhttpKeys}; +use payjoin::{receive::v2::SessionEvent as ReceiverSessionEvent, send::v2::SessionEvent as SenderSessionEvent, OhttpKeys}; use std::convert::TryInto; use std::{ From 4cded079e2bbf0abb1cd18d50222355fb3e33179 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 8 Jul 2025 11:28:21 -0400 Subject: [PATCH 14/72] Impl payjoin methods on lianad --- lianad/src/jsonrpc/api.rs | 46 +++++++++++++++++++++++++++++++++++++ lianad/src/payjoin/types.rs | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index 7f6082e6d..ab966b7e5 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -486,6 +486,39 @@ fn get_labels_bip329(control: &DaemonControl, params: Params) -> Result Result { + let res = control.receive_payjoin(); + Ok(serde_json::json!(&res)) +} + +fn send_payjoin(control: &DaemonControl, params: Params) -> Result { + let bip21 = params + .get(0, "bip21") + .ok_or_else(|| Error::invalid_params("Missing 'bip21' parameter."))? + .as_str() + .ok_or_else(|| Error::invalid_params("Invalid 'bip21' parameter."))?; + let psbt = params + .get(1, "psbt") + .ok_or_else(|| Error::invalid_params("Missing 'psbt' parameter."))? + .as_str() + .ok_or_else(|| Error::invalid_params("Invalid 'psbt' parameter."))?; + let psbt = + Psbt::from_str(psbt).map_err(|_| Error::invalid_params("Invalid 'psbt' parameter."))?; + control.init_payjoin_sender(bip21.to_string(), &psbt)?; + Ok(serde_json::json!({})) +} + +fn get_payjoin_info(control: &DaemonControl, params: Params) -> Result { + let txid = params + .get(0, "txid") + .ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))? + .as_str() + .ok_or_else(|| Error::invalid_params("Invalid 'txid' parameter."))?; + let txid = bitcoin::Txid::from_str(txid).map_err(|_| Error::invalid_params("Invalid 'txid' parameter."))?; + let res = control.get_payjoin_info(&txid)?; + Ok(serde_json::json!(&res)) +} + /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -593,6 +626,19 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result receive_payjoin(control)?, + "sendpayjoin" => { + let params = req + .params + .ok_or_else(|| Error::invalid_params("Missing 'payjoin_url' parameter."))?; + send_payjoin(control, params)? + } + "getpayjoininfo" => { + let params = req + .params + .ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))?; + get_payjoin_info(control, params)? + } _ => { return Err(Error::method_not_found()); } diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index ff106616e..27ee6cfe5 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -65,7 +65,7 @@ impl From for PayjoinStatus { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct PayjoinInfo { pub sender_status: Option, pub receiver_status: Option, From a9da8173868fa1a1d9631fcf0559bac28e0a71ff Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 8 Jul 2025 11:28:54 -0400 Subject: [PATCH 15/72] Should save amount and address from bip21 --- liana-gui/src/app/state/spend/step.rs | 6 ++++++ liana-gui/src/daemon/client/mod.rs | 11 ++++++----- liana/src/spend.rs | 3 +++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 729b27da6..3454edbc9 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -604,16 +604,22 @@ impl Step for DefineSpend { } view::CreateSpendMessage::Bip21Edited(i, bip21) => { + log::info!("Bip21Edited: {}", bip21); if let Some(recipient) = self.recipients.get_mut(i) { recipient.bip21.value = bip21.clone(); if let Ok(uri) = Uri::try_from(bip21.as_str()) { if let Ok(address) = uri.address.require_network(cache.network) { recipient.address.value = address.to_string(); + recipient.update(cache.network, view::CreateSpendMessage::RecipientEdited(i, "address", address.to_string())); } if let Some(amount) = uri.amount { + log::info!("Amount: {}", amount); recipient.amount.value = amount.to_string_in(Denomination::Bitcoin); + recipient.update(cache.network, view::CreateSpendMessage::RecipientEdited(i, "amount", amount.to_string_in(Denomination::Bitcoin))); } + } else { + self.warning = Some(SpendCreationError::InvalidBip21.into()); } } } diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index de70d1aa5..9b52f54ae 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -109,15 +109,16 @@ impl Daemon for Lianad { } async fn receive_payjoin(&self) -> Result { - unimplemented!() + self.call("receivepayjoin", Option::::None) } - async fn send_payjoin(&self, _bip21: String, _psbt: &Psbt) -> Result<(), DaemonError> { - unimplemented!() + async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError> { + let _res: serde_json::value::Value = self.call("sendpayjoin", Some(vec![bip21, psbt.to_string()]))?; + Ok(()) } - async fn get_payjoin_info(&self, _txid: &Txid) -> Result, DaemonError> { - unimplemented!() + async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { + self.call("getpayjoininfo", Some(vec![txid.to_string()])) } async fn update_deriv_indexes( diff --git a/liana/src/spend.rs b/liana/src/spend.rs index c4f350cfc..a55e2e601 100644 --- a/liana/src/spend.rs +++ b/liana/src/spend.rs @@ -55,6 +55,8 @@ pub enum SpendCreationError { SanityCheckFailure(Psbt), FetchingTransaction(bitcoin::OutPoint), CoinSelection(InsufficientFunds), + // TODO: wrap more specific error + InvalidBip21, } impl fmt::Display for SpendCreationError { @@ -84,6 +86,7 @@ impl fmt::Display for SpendCreationError { "BUG! Please report this. Failed sanity checks for PSBT '{}'.", psbt ), + Self::InvalidBip21 => write!(f, "Invalid BIP21"), } } } From 64a490b2d6cba5d557f85dbfce8146108873bb3b Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 10:39:15 -0400 Subject: [PATCH 16/72] Remove redundant results in db interface --- lianad/src/database/mod.rs | 5 ++++- lianad/src/payjoin/db.rs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index ec802fab5..d6a6b9280 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -23,7 +23,10 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; -use payjoin::{receive::v2::SessionEvent as ReceiverSessionEvent, send::v2::SessionEvent as SenderSessionEvent, OhttpKeys}; +use payjoin::{ + receive::v2::SessionEvent as ReceiverSessionEvent, + send::v2::SessionEvent as SenderSessionEvent, OhttpKeys, +}; /// Information about the wallet. /// diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 9dd75395d..51608f1c0 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -62,7 +62,7 @@ pub struct ReceiverPersister { } impl ReceiverPersister { - pub fn new(db: Arc) -> Result> { + pub fn new(db: Arc) -> Self { let mut db_conn = db.connection(); let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_receivers")); let session: SessionWrapper = SessionWrapper { @@ -76,11 +76,11 @@ impl ReceiverPersister { completed_at: None, }; db_conn.payjoin_save_receiver_session(&session_id, session); - Ok(Self { db, session_id }) + Self { db, session_id } } - pub fn from_id(db: Arc, id: SessionId) -> Result> { - Ok(Self { db, session_id: id }) + pub fn from_id(db: Arc, id: SessionId) -> Self { + Self { db, session_id: id } } pub fn update_metadata( @@ -149,7 +149,7 @@ pub struct SenderPersister { } impl SenderPersister { - pub fn new(db: Arc) -> Result> { + pub fn new(db: Arc) -> Self { let mut db_conn = db.connection(); let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_senders")); let session: SessionWrapper = SessionWrapper { @@ -163,11 +163,11 @@ impl SenderPersister { completed_at: None, }; db_conn.payjoin_save_sender_session(&session_id, session); - Ok(Self { db, session_id }) + Self { db, session_id } } - pub fn from_id(db: Arc, id: SessionId) -> Result> { - Ok(Self { db, session_id: id }) + pub fn from_id(db: Arc, id: SessionId) -> Self { + Self { db, session_id: id } } pub fn update_metadata( From 50981d7f8a07da1f2d4773213f12b1679fe962ed Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 11:31:13 -0400 Subject: [PATCH 17/72] Handle ohttp and finalize sender psbt error in rpc endpoints --- liana-gui/src/daemon/embedded.rs | 7 +++- lianad/src/commands/mod.rs | 41 ++++++++++++++----- lianad/src/jsonrpc/api.rs | 5 ++- lianad/src/jsonrpc/rpc.rs | 6 ++- lianad/src/payjoin/db.rs | 1 - lianad/src/payjoin/helpers.rs | 68 ++++++++++++++++++++++---------- lianad/src/payjoin/receiver.rs | 13 +++--- lianad/src/payjoin/sender.rs | 9 +++-- 8 files changed, 103 insertions(+), 47 deletions(-) diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 3d3f06463..4170fccfb 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -122,7 +122,12 @@ impl Daemon for EmbeddedDaemon { } async fn receive_payjoin(&self) -> Result { - self.command(|daemon| Ok(daemon.receive_payjoin())).await + self.command(|daemon| { + daemon + .receive_payjoin() + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await } async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError> { diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 26e6bf8b6..3cdccc15e 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -10,7 +10,7 @@ use crate::{ miniscript::bitcoin::absolute::LockTime, payjoin::{ db::{ReceiverPersister, SenderPersister, SessionId, SessionMetadata, SessionWrapper}, - helpers::fetch_ohttp_keys, + helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, types::{PayjoinInfo, PayjoinStatus}, }, poller::PollerMessage, @@ -85,6 +85,9 @@ pub enum CommandError { InvalidDerivationIndex, RbfError(RbfErrorInfo), EmptyFilterList, + FailedToFetchOhttpKeys(FetchOhttpKeysError), + // Same FIXME as `SpendFinalization` + FailedToPostOriginalPayjoinProposal(String), } impl fmt::Display for CommandError { @@ -143,6 +146,10 @@ impl fmt::Display for CommandError { } Self::RbfError(e) => write!(f, "RBF error: '{}'.", e), Self::EmptyFilterList => write!(f, "Filter list is empty, should supply None instead."), + Self::FailedToFetchOhttpKeys(e) => write!(f, "Failed to fetch OHTTP keys: '{}'.", e), + Self::FailedToPostOriginalPayjoinProposal(e) => { + write!(f, "Failed to post original payjoin proposal: '{}'.", e) + } } } } @@ -373,7 +380,7 @@ impl DaemonControl { GetAddressResult::new(address, new_index, "".to_string()) } - pub fn receive_payjoin(&self) -> GetAddressResult { + pub fn receive_payjoin(&self) -> Result { let mut db_conn = self.db.connection(); // TODO(arturgontijo): Fetch these from DB (via GUI's Settings Panel) @@ -386,7 +393,7 @@ impl DaemonControl { let ohttp_keys = std::thread::spawn(move || fetch_ohttp_keys(ohttp_relay, directory)) .join() .unwrap() - .unwrap(); + .map_err(CommandError::FailedToFetchOhttpKeys)?; db_conn.payjoin_save_ohttp_keys(ohttp_relay, ohttp_keys.clone()); ohttp_keys }; @@ -403,7 +410,7 @@ impl DaemonControl { .derive(new_index, &self.secp) .address(self.config.bitcoin_config.network); - let persister = ReceiverPersister::new(Arc::new(self.db.clone())).unwrap(); + let persister = ReceiverPersister::new(Arc::new(self.db.clone())); let session = Receiver::::create_session( address.clone(), directory, @@ -413,11 +420,18 @@ impl DaemonControl { .save(&persister) .unwrap(); - GetAddressResult::new(address, new_index, session.pj_uri().to_string()) + Ok(GetAddressResult::new( + address, + new_index, + session.pj_uri().to_string(), + )) } /// Initiate a payjoin sender + // TODO bip21 should be a uri not a string + // TODO: min fee rate should be a param pub fn init_payjoin_sender(&self, bip21: String, psbt: &Psbt) -> Result<(), CommandError> { + // TODO: validate bip21 in uri let uri = Uri::try_from(bip21.clone()) .map_err(|e| format!("Failed to create URI from BIP21: {}", e)) .unwrap(); @@ -428,9 +442,11 @@ impl DaemonControl { .unwrap(); let mut psbt = psbt.clone(); - psbt.finalize_mut(&Secp256k1::verification_only()).unwrap(); + psbt.finalize_mut(&Secp256k1::verification_only()) + // Just display the first error + .map_err(|e| CommandError::FailedToPostOriginalPayjoinProposal(e[0].to_string()))?; - let persister = SenderPersister::new(Arc::new(self.db.clone())).unwrap(); + let persister = SenderPersister::new(Arc::new(self.db.clone())); let _sender = SenderBuilder::new(psbt.clone(), uri) .build_recommended(FeeRate::BROADCAST_MIN) .save(&persister) @@ -447,6 +463,7 @@ impl DaemonControl { } /// Get Payjoin URI (BIP21) and its sender/receiver status by txid + /// TODO: this seems unused, can we remove it? pub fn get_payjoin_info(&self, txid: &Txid) -> Result, CommandError> { let mut db_conn = self.db.connection(); @@ -492,13 +509,16 @@ impl DaemonControl { } } - pub fn payjoin_get_all_receiver_sessions(&self) -> Vec<(SessionId, SessionWrapper)> { + pub fn payjoin_get_all_receiver_sessions( + &self, + ) -> Vec<(SessionId, SessionWrapper)> { let mut db_conn = self.db.connection(); db_conn.payjoin_get_all_receiver_sessions() } - pub fn payjoin_get_receiver_session(&self, session_id: &SessionId) -> Option { - ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()).ok() + // TODO: is this unused? Can we remove it? + pub fn payjoin_get_receiver_session(&self, session_id: &SessionId) -> ReceiverPersister { + ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()) } /// Update derivation indexes @@ -1517,6 +1537,7 @@ impl GetAddressResult { pub fn new( address: bitcoin::Address, derivation_index: bip32::ChildNumber, + // TODO: rename to bip21 payjoin_uri: String, ) -> Self { Self { diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index ab966b7e5..a4196f974 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -487,7 +487,7 @@ fn get_labels_bip329(control: &DaemonControl, params: Params) -> Result Result { - let res = control.receive_payjoin(); + let res = control.receive_payjoin()?; Ok(serde_json::json!(&res)) } @@ -514,7 +514,8 @@ fn get_payjoin_info(control: &DaemonControl, params: Params) -> Result for Error { | commands::CommandError::RbfError(..) | commands::CommandError::EmptyFilterList | commands::CommandError::RecoveryNotAvailable - | commands::CommandError::OutpointNotRecoverable(..) => { + | commands::CommandError::OutpointNotRecoverable(..) + | commands::CommandError::FailedToFetchOhttpKeys(..) => { Error::new(ErrorCode::InvalidParams, e.to_string()) } commands::CommandError::RescanTrigger(..) => { @@ -173,6 +174,9 @@ impl From for Error { commands::CommandError::TxBroadcast(_) => { Error::new(ErrorCode::ServerError(BROADCAST_ERROR), e.to_string()) } + commands::CommandError::FailedToPostOriginalPayjoinProposal(_) => { + Error::new(ErrorCode::ServerError(BROADCAST_ERROR), e.to_string()) + } } } } diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 51608f1c0..1b3cdd2c6 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -3,7 +3,6 @@ use payjoin::persist::SessionPersister; use payjoin::receive::v2::SessionEvent as ReceiverSessionEvent; use payjoin::send::v2::SessionEvent as SenderSessionEvent; use serde::{Deserialize, Serialize}; -use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::SystemTime; diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 5bd9efa30..349661f63 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -1,4 +1,4 @@ -use std::{error::Error, time::Duration}; +use std::time::Duration; use miniscript::{ bitcoin::{secp256k1, Psbt, ScriptBuf, TxOut}, @@ -14,48 +14,74 @@ pub fn http_agent() -> reqwest::blocking::Client { reqwest::blocking::Client::new() } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FetchOhttpKeysError { + Reqwest(String), + InvalidOhttpKeys(String), + InvalidUrl(String), + UrlParseError, + UnexpectedStatusCode(reqwest::StatusCode), +} + +impl std::error::Error for FetchOhttpKeysError {} +impl std::fmt::Display for FetchOhttpKeysError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + pub fn fetch_ohttp_keys( ohttp_relay: impl IntoUrl, payjoin_directory: impl IntoUrl, -) -> Result> { - let ohttp_keys_url = payjoin_directory - .into_url()? - .join("/.well-known/ohttp-gateway")?; - let proxy = Proxy::all(ohttp_relay.into_url()?.as_str())?; - let client = reqwest::blocking::Client::builder().proxy(proxy).build()?; +) -> Result { + let payjoin_directory_str = payjoin_directory.as_str().to_string(); + let payjoin_directory_url = payjoin_directory + .into_url() + .map_err(|_| FetchOhttpKeysError::InvalidUrl(payjoin_directory_str.clone()))? + .join("/.well-known/ohttp-gateway") + .map_err(|_| FetchOhttpKeysError::UrlParseError)?; + + let ohttp_relay_str = ohttp_relay.as_str().to_string(); + let proxy = Proxy::all( + ohttp_relay + .into_url() + .map_err(|_| FetchOhttpKeysError::InvalidUrl(ohttp_relay_str.clone()))? + .as_str(), + ) + .map_err(|e| FetchOhttpKeysError::Reqwest(e.to_string()))?; + let client = reqwest::blocking::Client::builder() + .proxy(proxy) + .build() + .map_err(|e| FetchOhttpKeysError::Reqwest(e.to_string()))?; let res = client - .get(ohttp_keys_url) + .get(payjoin_directory_url) .header(ACCEPT, "application/ohttp-keys") - .send()?; - parse_ohttp_keys_response(res) + .send() + .map_err(|e| FetchOhttpKeysError::Reqwest(e.to_string()))?; + Ok(validate_ohttp_keys_response(res)?) } -fn parse_ohttp_keys_response( +fn validate_ohttp_keys_response( res: reqwest::blocking::Response, -) -> Result> { +) -> Result { if !res.status().is_success() { - return Err(format!("UnexpectedStatusCode: {}", res.status()).into()); + return Err(FetchOhttpKeysError::UnexpectedStatusCode(res.status())); } let body = res.bytes().unwrap().to_vec(); match OhttpKeys::decode(&body) { Ok(ohttp_keys) => Ok(ohttp_keys), - Err(err) => Err(format!("InvalidOhttpKeys: {}", err).into()), + Err(err) => Err(FetchOhttpKeysError::InvalidOhttpKeys(err.to_string())), } } -pub fn post_request(req: payjoin::Request) -> Result> { +pub fn post_request(req: payjoin::Request) -> Result { let http = http_agent(); - match http - .post(req.url) + http.post(req.url) .header("Content-Type", req.content_type) .body(req.body) .timeout(Duration::from_secs(10)) .send() - { - Ok(r) => Ok(r), - Err(err) => Err(format!("Failed to post_reques(): {}", err).into()), - } } pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1) { diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 753c1a668..56c8081d8 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -8,14 +8,14 @@ use liana::descriptors; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, - TxIn, + consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, TxIn, }, persist::OptionalTransitionOutcome, receive::{ v2::{ - replay_event_log, Initialized, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, PayjoinProposal, ProvisionalProposal, - ReceiveSession, Receiver, SessionHistory, UncheckedProposal, WantsInputs, WantsOutputs + replay_event_log, Initialized, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, + PayjoinProposal, ProvisionalProposal, ReceiveSession, Receiver, SessionHistory, + UncheckedProposal, WantsInputs, WantsOutputs, }, InputPair, }, @@ -63,7 +63,7 @@ fn read_from_directory( Err(e) => return Err(e.into()), } } - Err(e) => return Err(e), + Err(e) => return Err(Box::new(e)), }; check_proposal(proposal, persister, db_conn, bit, desc, secp) } @@ -304,8 +304,7 @@ fn process_receiver_session( log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); - let persister = - ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); + let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()); let (state, history) = replay_event_log(&persister) .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 93790960b..3afb500df 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -9,8 +9,8 @@ use std::sync::{self, Arc}; use payjoin::bitcoin::Psbt; use payjoin::persist::OptionalTransitionOutcome; -use payjoin::send::v2::{Sender, WithReplyKey}; use payjoin::send::v2::{replay_event_log, SendSession, V2GetContext}; +use payjoin::send::v2::{Sender, WithReplyKey}; use super::db::SenderPersister; use super::helpers::OHTTP_RELAY; @@ -19,6 +19,7 @@ use super::types::PayjoinStatus; fn get_proposed_payjoin_psbt( context: Sender, persister: &SenderPersister, + // TODO: replace with specific error ) -> Result, Box> { let (req, ctx) = context.extract_req(OHTTP_RELAY)?; match post_request(req) { @@ -41,7 +42,7 @@ fn get_proposed_payjoin_psbt( } } } - Err(e) => Err(e), + Err(e) => Err(Box::new(e)), } } @@ -58,7 +59,7 @@ fn post_orginal_proposal( .save(persister)?; Ok(()) } - Err(e) => Err(e), + Err(e) => Err(Box::new(e)), } } @@ -107,7 +108,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); - let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()).unwrap(); + let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()); let (state, _) = replay_event_log(&persister) .map_err(|e| format!("Failed to replay sender event log: {:?}", e)) From 7465e63cdd48791863b70cf9d063ef8aae2f0de6 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 11:31:38 -0400 Subject: [PATCH 18/72] Run project wide fmt --- liana-gui/src/app/state/spend/step.rs | 18 ++++++++++++++++-- liana-gui/src/daemon/client/mod.rs | 3 ++- lianad/src/database/sqlite/mod.rs | 5 ++++- lianad/src/testutils.rs | 5 ++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 3454edbc9..d1e84edc7 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -610,13 +610,27 @@ impl Step for DefineSpend { if let Ok(uri) = Uri::try_from(bip21.as_str()) { if let Ok(address) = uri.address.require_network(cache.network) { recipient.address.value = address.to_string(); - recipient.update(cache.network, view::CreateSpendMessage::RecipientEdited(i, "address", address.to_string())); + recipient.update( + cache.network, + view::CreateSpendMessage::RecipientEdited( + i, + "address", + address.to_string(), + ), + ); } if let Some(amount) = uri.amount { log::info!("Amount: {}", amount); recipient.amount.value = amount.to_string_in(Denomination::Bitcoin); - recipient.update(cache.network, view::CreateSpendMessage::RecipientEdited(i, "amount", amount.to_string_in(Denomination::Bitcoin))); + recipient.update( + cache.network, + view::CreateSpendMessage::RecipientEdited( + i, + "amount", + amount.to_string_in(Denomination::Bitcoin), + ), + ); } } else { self.warning = Some(SpendCreationError::InvalidBip21.into()); diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 9b52f54ae..50cd30c71 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -113,7 +113,8 @@ impl Daemon for Lianad { } async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError> { - let _res: serde_json::value::Value = self.call("sendpayjoin", Some(vec![bip21, psbt.to_string()]))?; + let _res: serde_json::value::Value = + self.call("sendpayjoin", Some(vec![bip21, psbt.to_string()]))?; Ok(()) } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index a0597bf1f..303c34f81 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -28,7 +28,10 @@ use crate::{ payjoin::db::{SessionId, SessionWrapper}, }; use liana::descriptors::LianaDescriptor; -use payjoin::{receive::v2::SessionEvent as ReceiverSessionEvent, send::v2::SessionEvent as SenderSessionEvent, OhttpKeys}; +use payjoin::{ + receive::v2::SessionEvent as ReceiverSessionEvent, + send::v2::SessionEvent as SenderSessionEvent, OhttpKeys, +}; use std::{ cmp, diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 717d3c7ad..5deec016b 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -9,7 +9,10 @@ use crate::{ DaemonControl, DaemonHandle, }; use liana::descriptors; -use payjoin::{receive::v2::SessionEvent as ReceiverSessionEvent, send::v2::SessionEvent as SenderSessionEvent, OhttpKeys}; +use payjoin::{ + receive::v2::SessionEvent as ReceiverSessionEvent, + send::v2::SessionEvent as SenderSessionEvent, OhttpKeys, +}; use std::convert::TryInto; use std::{ From 357138b2b2c6649fe64691d4a1c505ee47a278c9 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 11:56:43 -0400 Subject: [PATCH 19/72] Remove get payjoin info endpoint --- liana-gui/src/daemon/client/mod.rs | 5 -- liana-gui/src/daemon/embedded.rs | 10 ---- liana-gui/src/daemon/mod.rs | 12 ++--- .../services/connect/client/backend/mod.rs | 5 -- lianad/src/commands/mod.rs | 53 ++----------------- lianad/src/jsonrpc/api.rs | 18 ------- 6 files changed, 9 insertions(+), 94 deletions(-) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 50cd30c71..ecbf12791 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,7 +5,6 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; -use lianad::payjoin::types::PayjoinInfo; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -118,10 +117,6 @@ impl Daemon for Lianad { Ok(()) } - async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { - self.call("getpayjoininfo", Some(vec![txid.to_string()])) - } - async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 4170fccfb..e0f52b841 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,6 +1,5 @@ use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; -use lianad::payjoin::types::PayjoinInfo; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; @@ -139,15 +138,6 @@ impl Daemon for EmbeddedDaemon { .await } - async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { - self.command(|daemon| { - daemon - .get_payjoin_info(txid) - .map_err(|e| DaemonError::Unexpected(e.to_string())) - }) - .await - } - async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index ef7c00e80..960ea1d80 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -18,7 +18,6 @@ use liana::miniscript::bitcoin::{ }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; -use lianad::payjoin::types::PayjoinInfo; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -117,7 +116,6 @@ pub trait Daemon: Debug { ) -> Result; async fn receive_payjoin(&self) -> Result; async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; - async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError>; async fn update_deriv_indexes( &self, receive: Option, @@ -212,9 +210,11 @@ pub trait Daemon: Debug { .cloned() .collect(); - let payjoin_info = self - .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) - .await?; + // TODO: get payjoin info if a session exists for this transaction + // Status should be derived from replaying the session + // let payjoin_info = self + // .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) + // .await?; spend_txs.push(model::SpendTx::new( tx.updated_at, @@ -223,7 +223,7 @@ pub trait Daemon: Debug { &info.descriptors.main, &curve, info.network, - payjoin_info, + None, // Payjoin status )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index f16dfc339..45d10b620 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -17,7 +17,6 @@ use lianad::{ bip329::Labels, commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, - payjoin::types::PayjoinInfo, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; @@ -632,10 +631,6 @@ impl Daemon for BackendWalletClient { unimplemented!() } - async fn get_payjoin_info(&self, _txid: &Txid) -> Result, DaemonError> { - unimplemented!() - } - async fn list_revealed_addresses( &self, is_change: bool, diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 3cdccc15e..a17a45b49 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -9,9 +9,9 @@ use crate::{ database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, payjoin::{ - db::{ReceiverPersister, SenderPersister, SessionId, SessionMetadata, SessionWrapper}, + db::{ReceiverPersister, SenderPersister, SessionId, SessionWrapper}, helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, - types::{PayjoinInfo, PayjoinStatus}, + types::PayjoinStatus, }, poller::PollerMessage, DaemonControl, VERSION, @@ -49,7 +49,7 @@ use miniscript::{ psbt::PsbtExt, }; use payjoin::{ - bitcoin::{key::Secp256k1, FeeRate, Txid}, + bitcoin::{key::Secp256k1, FeeRate}, receive::v2::{Receiver, SessionEvent as ReceiverSessionEvent, UninitializedReceiver}, send::v2::SenderBuilder, Uri, UriExt, @@ -462,53 +462,6 @@ impl DaemonControl { Ok(()) } - /// Get Payjoin URI (BIP21) and its sender/receiver status by txid - /// TODO: this seems unused, can we remove it? - pub fn get_payjoin_info(&self, txid: &Txid) -> Result, CommandError> { - let mut db_conn = self.db.connection(); - - let mut receiver_status = None; - for (_, session) in db_conn.payjoin_get_all_receiver_sessions() { - let SessionMetadata { - status, maybe_txid, .. - } = session.metadata.clone(); - if let Some(db_txid) = maybe_txid { - if &db_txid == txid { - receiver_status = Some(status); - break; - } - } - } - - let mut bip21 = String::new(); - let mut sender_status = None; - for (_, session) in db_conn.payjoin_get_all_sender_sessions() { - let SessionMetadata { - status, - maybe_txid, - maybe_bip21, - .. - } = session.metadata.clone(); - if let Some(db_txid) = maybe_txid { - if &db_txid == txid { - sender_status = Some(status); - bip21 = maybe_bip21.unwrap_or_default(); - break; - } - } - } - - if receiver_status.is_some() || sender_status.is_some() { - Ok(Some(PayjoinInfo { - bip21, - sender_status, - receiver_status, - })) - } else { - Ok(None) - } - } - pub fn payjoin_get_all_receiver_sessions( &self, ) -> Vec<(SessionId, SessionWrapper)> { diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index a4196f974..dce46a5fe 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -508,18 +508,6 @@ fn send_payjoin(control: &DaemonControl, params: Params) -> Result Result { - let txid = params - .get(0, "txid") - .ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))? - .as_str() - .ok_or_else(|| Error::invalid_params("Invalid 'txid' parameter."))?; - let txid = bitcoin::Txid::from_str(txid) - .map_err(|_| Error::invalid_params("Invalid 'txid' parameter."))?; - let res = control.get_payjoin_info(&txid)?; - Ok(serde_json::json!(&res)) -} - /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -634,12 +622,6 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { - let params = req - .params - .ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))?; - get_payjoin_info(control, params)? - } _ => { return Err(Error::method_not_found()); } From afdbacd623051d2dadc7f2e86438936ad08c35d9 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 12:38:28 -0400 Subject: [PATCH 20/72] Simplify status to wrap only one enum --- liana-gui/src/app/state/psbt.rs | 2 +- liana-gui/src/app/state/spend/step.rs | 3 +-- liana-gui/src/app/view/psbt.rs | 2 +- liana-gui/src/daemon/model.rs | 2 +- lianad/src/payjoin/types.rs | 3 +-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index ce78dcf8d..a4fdcc30d 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -588,7 +588,7 @@ impl Modal for SignModal { // TODO(arturgontijo): Use better design. Maybe checking for foreign inputs. if let Some(payjoin_info) = &tx.payjoin_info { - if payjoin_info.receiver_status == Some(PayjoinStatus::Signing) { + if payjoin_info.status == Some(PayjoinStatus::Signing) { tx.status = SpendStatus::PayjoinProposalReady; } } diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index d1e84edc7..be40c8307 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -994,8 +994,7 @@ impl Step for SaveSpend { } else { Some(PayjoinInfo { bip21, - sender_status: None, - receiver_status: None, + status: None, }) }; diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index 4c04333fa..97746bee0 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -424,7 +424,7 @@ pub fn spend_overview_view<'a>( }) .push_maybe(if tx.path_ready().is_some() { if let Some(payjoin_info) = &tx.payjoin_info { - if payjoin_info.sender_status.is_none() { + if payjoin_info.status.is_none() { Some( button::secondary(None, "Send Payjoin") .on_press(Message::Spend(SpendTxMessage::SendPayjoin)) diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 7794a02f4..57fcc4e26 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -205,7 +205,7 @@ impl SpendTx { // TODO(arturgontijo): We should count the sigs, just in case. if let Some(payjoin_info) = &self.payjoin_info { - if let Some(PayjoinStatus::Completed) = payjoin_info.sender_status { + if let Some(PayjoinStatus::Completed) = payjoin_info.status { let has_sigs = self .psbt .inputs diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index 27ee6cfe5..91d6ca4cd 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -67,7 +67,6 @@ impl From for PayjoinStatus { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PayjoinInfo { - pub sender_status: Option, - pub receiver_status: Option, + pub status: Option, pub bip21: String, } From c949090112413faafa12c90fa6db9babfccef31e Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 12:41:17 -0400 Subject: [PATCH 21/72] Remove unused receiver sender status enums --- lianad/src/payjoin/types.rs | 42 ------------------------------------- 1 file changed, 42 deletions(-) diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index 91d6ca4cd..4e3a8e6cb 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -1,47 +1,5 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PayjoinSenderStatus { - Pending = 0, - WaitingReceiver = 1, - Completed = 2, - // TODO: more specific enums for why it failed - Failed = 3, -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub enum PayjoinReceiverStatus { - Pending = 0, - Signing = 1, - Completed = 2, - // TODO: more specific enums for why it failed - Failed = 3, -} - -impl From for PayjoinSenderStatus { - fn from(status: i32) -> Self { - match status { - 0 => PayjoinSenderStatus::Pending, - 1 => PayjoinSenderStatus::WaitingReceiver, - 2 => PayjoinSenderStatus::Completed, - 3 => PayjoinSenderStatus::Failed, - _ => panic!("Invalid payjoin sender status: {}", status), - } - } -} - -impl From for PayjoinReceiverStatus { - fn from(status: i32) -> Self { - match status { - 0 => PayjoinReceiverStatus::Pending, - 1 => PayjoinReceiverStatus::Signing, - 2 => PayjoinReceiverStatus::Completed, - 3 => PayjoinReceiverStatus::Failed, - _ => panic!("Invalid payjoin receiver status: {}", status), - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PayjoinStatus { Pending = 0, From 1aec1a95265daebe167309e28432f8c5585a8042 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 10 Jul 2025 11:10:11 -0400 Subject: [PATCH 22/72] Update pdk branch to use-witness-for-expected-weight This also updated the receiver to derive input weight when constructing inputs --- Cargo.lock | 2 +- liana-gui/Cargo.toml | 2 +- lianad/Cargo.toml | 2 +- lianad/src/payjoin/receiver.rs | 12 ++++++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eafc582bf..68d97edd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,7 +4285,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.24.0" -source = "git+https://github.com/arturgontijo/rust-payjoin.git?branch=p2wsh-fix#353dd8ab6aace31713d97d0e348c02197273a2af" +source = "git+https://github.com/arminsabouri/rust-payjoin.git?branch=use-witness-for-expected-weight#2901576ddf73a5720f8aa0a7632201f196db9cd0" dependencies = [ "bhttp", "bitcoin", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 410c475d4..93745ef09 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,7 +55,7 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "p2wsh-fix", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "use-witness-for-expected-weight", features = ["v2", "io"] } [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 2002fa663..dcd9c040e 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,5 +60,5 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -payjoin = { git = "https://github.com/arturgontijo/rust-payjoin.git", branch = "p2wsh-fix", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "use-witness-for-expected-weight", features = ["v2", "io"] } reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 56c8081d8..be7a93b8f 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -8,7 +8,8 @@ use liana::descriptors; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, TxIn, + consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, + TxIn, Weight, }, persist::OptionalTransitionOutcome, receive::{ @@ -148,7 +149,7 @@ fn contribute_inputs( ) -> Result<(), Box> { let coins = db_conn.coins(&[CoinStatus::Confirmed], &[]); - let mut candidate_inputs_map = HashMap::::new(); + let mut candidate_inputs_map = HashMap::::new(); for (outpoint, coin) in coins.iter() { let txs = db_conn.list_wallet_transactions(&[outpoint.txid]); let (db_tx, _, _) = txs.first().unwrap(); @@ -177,13 +178,16 @@ fn contribute_inputs( }; derived_desc.update_psbt_in(&mut psbtin); + let worse_case_weight = Weight::from_wu_usize(desc.max_sat_weight(true)); - candidate_inputs_map.insert(*outpoint, (*coin, txin, psbtin)); + candidate_inputs_map.insert(*outpoint, (*coin, txin, psbtin, worse_case_weight)); } let candidate_inputs = candidate_inputs_map .values() - .map(|(_, txin, psbtin)| InputPair::new(txin.clone(), psbtin.clone()).unwrap()); + .map(|(_, txin, psbtin, weight)| { + InputPair::new(txin.clone(), psbtin.clone(), Some(*weight)).unwrap() + }); let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); From cabadb12fd5a815bc659841806d41923fb490927 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 9 Jul 2025 14:02:24 -0400 Subject: [PATCH 23/72] Cleanup receiver unwraps and error propogation Just a small commit to begin cleaning up some unhandled unwraps and begin to move away from the reliance on the session metadata. --- lianad/src/payjoin/receiver.rs | 50 +++++++++++++--------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index be7a93b8f..be8a566bb 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -2,14 +2,14 @@ use std::{ collections::HashMap, error::Error, sync::{self, Arc}, + time::SystemTime, }; use liana::descriptors; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, - TxIn, Weight, + consensus::encode::serialize_hex, psbt::Input, secp256k1, OutPoint, Sequence, TxIn, Weight, }, persist::OptionalTransitionOutcome, receive::{ @@ -25,10 +25,7 @@ use payjoin::{ use crate::{ bitcoin::BitcoinInterface, database::{Coin, CoinStatus, DatabaseConnection, DatabaseInterface}, - payjoin::{ - db::SessionMetadata, - helpers::{finalize_psbt, post_request, OHTTP_RELAY}, - }, + payjoin::helpers::{finalize_psbt, post_request, OHTTP_RELAY}, }; use super::{db::ReceiverPersister, types::PayjoinStatus}; @@ -47,14 +44,9 @@ fn read_from_directory( .expect("Failed to extract request"); let proposal = match post_request(req.clone()) { Ok(ohttp_response) => { + let response_bytes = ohttp_response.bytes()?; let state_transition = receiver - .process_res( - ohttp_response - .bytes() - .expect("Failed to read response") - .as_ref(), - context, - ) + .process_res(response_bytes.as_ref(), context) .save(persister); match state_transition { Ok(OptionalTransitionOutcome::Progress(next_state)) => next_state, @@ -152,7 +144,9 @@ fn contribute_inputs( let mut candidate_inputs_map = HashMap::::new(); for (outpoint, coin) in coins.iter() { let txs = db_conn.list_wallet_transactions(&[outpoint.txid]); - let (db_tx, _, _) = txs.first().unwrap(); + let (db_tx, _, _) = txs + .first() + .expect("There should be at least tx in the wallet"); let tx = db_tx.clone(); @@ -236,14 +230,15 @@ fn finalize_proposal( } if is_signed { - let mut psbt = psbt.clone(); - finalize_psbt(&mut psbt, secp); - let proposal = proposal .finalize_proposal( - |_| Ok(psbt.clone()), + |_| { + let mut psbt = psbt.clone(); + finalize_psbt(&mut psbt, secp); + Ok(psbt) + }, + None, None, - Some(FeeRate::from_sat_per_vb(150).unwrap()), ) .save(persister)?; @@ -295,24 +290,17 @@ fn process_receiver_session( ) -> Result<(), Box> { let mut db_conn = db.connection(); for (session_id, session) in db_conn.payjoin_get_all_receiver_sessions() { - let SessionMetadata { - status, - maybe_bip21, - .. - } = session.metadata.clone(); - - // No need to check Completed - if status == PayjoinStatus::Completed { + // No need to check past completed payjoins + if session.completed_at.is_some() + && SystemTime::now() > session.completed_at.expect("Some system time exists") + { continue; } - log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); - let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()); let (state, history) = replay_event_log(&persister) - .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) - .unwrap(); + .map_err(|e| format!("Failed to replay receiver event log: {:?}", e))?; match state { ReceiveSession::Initialized(context) => { From a907cb20476dbfe5393cc52ef393e47fc2c6e601 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 14:38:34 -0400 Subject: [PATCH 24/72] Refactor pj db tables to use seperate table for events --- lianad/src/commands/mod.rs | 20 ++-- lianad/src/database/mod.rs | 134 ++++++++--------------- lianad/src/database/sqlite/mod.rs | 153 ++++++++++++--------------- lianad/src/database/sqlite/schema.rs | 24 ++++- lianad/src/payjoin/db.rs | 129 ++++------------------ lianad/src/payjoin/receiver.rs | 48 ++------- lianad/src/payjoin/sender.rs | 126 +++++++++------------- lianad/src/testutils.rs | 57 ++++------ 8 files changed, 241 insertions(+), 450 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index a17a45b49..f865e13da 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -9,9 +9,8 @@ use crate::{ database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, payjoin::{ - db::{ReceiverPersister, SenderPersister, SessionId, SessionWrapper}, + db::{ReceiverPersister, SenderPersister, SessionId}, helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, - types::PayjoinStatus, }, poller::PollerMessage, DaemonControl, VERSION, @@ -50,7 +49,7 @@ use miniscript::{ }; use payjoin::{ bitcoin::{key::Secp256k1, FeeRate}, - receive::v2::{Receiver, SessionEvent as ReceiverSessionEvent, UninitializedReceiver}, + receive::v2::{Receiver, UninitializedReceiver}, send::v2::SenderBuilder, Uri, UriExt, }; @@ -447,26 +446,19 @@ impl DaemonControl { .map_err(|e| CommandError::FailedToPostOriginalPayjoinProposal(e[0].to_string()))?; let persister = SenderPersister::new(Arc::new(self.db.clone())); + log::info!("Saving new sender: {:?}", persister.session_id); let _sender = SenderBuilder::new(psbt.clone(), uri) .build_recommended(FeeRate::BROADCAST_MIN) .save(&persister) .unwrap(); - persister.update_metadata( - Some(PayjoinStatus::Pending), - Some(psbt.unsigned_tx.compute_txid()), - Some(psbt.clone()), - Some(bip21.clone()), - ); - Ok(()) } - pub fn payjoin_get_all_receiver_sessions( - &self, - ) -> Vec<(SessionId, SessionWrapper)> { + // TODO: this seems unsued can we remove? + pub fn payjoin_get_all_receiver_sessions(&self) -> Vec { let mut db_conn = self.db.connection(); - db_conn.payjoin_get_all_receiver_sessions() + db_conn.get_all_receiver_session_ids() } // TODO: is this unused? Can we remove it? diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index d6a6b9280..85baa94a6 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -10,7 +10,7 @@ use crate::{ schema::{DbBlockInfo, DbCoin, DbTip}, SqliteConn, SqliteDb, }, - payjoin::db::{SessionId, SessionWrapper}, + payjoin::db::SessionId, }; use std::{ @@ -23,10 +23,7 @@ use std::{ use bip329::Labels; use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; -use payjoin::{ - receive::v2::SessionEvent as ReceiverSessionEvent, - send::v2::SessionEvent as SenderSessionEvent, OhttpKeys, -}; +use payjoin::OhttpKeys; /// Information about the wallet. /// @@ -200,8 +197,6 @@ pub trait DatabaseConnection { /// Dump all labels fn get_labels_bip329(&mut self, offset: u32, limit: u32) -> Labels; - /// Payjoin - /// Get the next Session Id fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)>; @@ -212,55 +207,34 @@ pub trait DatabaseConnection { fn payjoin_next_id(&mut self, table: &str) -> u64; /// Save Receiver Session - fn payjoin_save_receiver_session( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ); - - /// Get a Receiver Session by Id - fn payjoin_get_receiver_session( - &mut self, - session_id: &SessionId, - ) -> Option>; + fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId); /// Get all Receiver Sessions - fn payjoin_get_all_receiver_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)>; + fn get_all_receiver_session_ids(&mut self) -> Vec; - /// Update the status of a payjoin receiver - fn update_payjoin_receiver_status( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ); + /// Save a Receiver Session Event + fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec); - /// Create a payjoin sender - fn payjoin_save_sender_session( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ); + /// Update completed at timestamp for a Receiver Session + /// Sets completed_at to current timestamp + fn update_receiver_session_completed_at(&mut self, session_id: &SessionId); - fn payjoin_get_sender_session( - &mut self, - session_id: &SessionId, - ) -> Option>; + /// Load all receiver session events for a particular session id + fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec>; + /// Create a payjoin sender + fn save_new_payjoin_sender_session(&mut self, session_id: &SessionId); /// Get a all active payjoin senders - fn payjoin_get_all_sender_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)>; + fn get_all_sender_session_ids(&mut self) -> Vec; - /// Update the status of a payjoin sender - fn update_payjoin_sender_status( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ); + /// Save a sender session event + fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec); - // ------- + /// Get all sender session events for a particular session id + fn get_all_sender_session_events(&mut self, session_id: &SessionId) -> Vec>; + + /// Update the completed at timestamp for a sender session + fn update_sender_session_completed_at(&mut self, session_id: &SessionId); } impl DatabaseConnection for SqliteConn { @@ -496,62 +470,44 @@ impl DatabaseConnection for SqliteConn { self.payjoin_next_id(table) } - fn payjoin_save_receiver_session( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - self.payjoin_save_receiver_session(session_id, session) + fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId) { + self.save_new_payjoin_receiver_session(session_id) } - fn payjoin_get_receiver_session( - &mut self, - session_id: &SessionId, - ) -> Option> { - self.payjoin_get_receiver_session(session_id) + fn get_all_receiver_session_ids(&mut self) -> Vec { + self.get_all_receiver_session_ids() } - fn payjoin_get_all_receiver_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)> { - self.payjoin_get_all_receiver_sessions() + fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec) { + self.save_receiver_session_event(session_id, event) } - fn update_payjoin_receiver_status( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - self.update_payjoin_receiver_status(session_id, session) + fn update_receiver_session_completed_at(&mut self, session_id: &SessionId) { + self.update_receiver_session_completed_at(session_id) } - fn payjoin_save_sender_session( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - self.payjoin_save_sender_session(session_id, session) + fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec> { + self.load_receiver_session_events(session_id) } - fn payjoin_get_sender_session( - &mut self, - session_id: &SessionId, - ) -> Option> { - self.payjoin_get_sender_session(session_id) + fn save_new_payjoin_sender_session(&mut self, session_id: &SessionId) { + self.save_new_payjoin_sender_session(session_id) } - fn payjoin_get_all_sender_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)> { - self.payjoin_get_all_sender_sessions() + fn get_all_sender_session_ids(&mut self) -> Vec { + self.get_all_sender_session_ids() } - fn update_payjoin_sender_status( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - self.update_payjoin_sender_status(session_id, session) + fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec) { + self.save_sender_session_event(session_id, event) + } + + fn get_all_sender_session_events(&mut self, session_id: &SessionId) -> Vec> { + self.load_sender_session_events(session_id) + } + + fn update_sender_session_completed_at(&mut self, session_id: &SessionId) { + self.update_sender_session_completed_at(session_id) } } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 303c34f81..adda9e229 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -25,13 +25,10 @@ use crate::{ }, Coin, CoinStatus, LabelItem, }, - payjoin::db::{SessionId, SessionWrapper}, + payjoin::db::SessionId, }; use liana::descriptors::LianaDescriptor; -use payjoin::{ - receive::v2::SessionEvent as ReceiverSessionEvent, - send::v2::SessionEvent as SenderSessionEvent, OhttpKeys, -}; +use payjoin::OhttpKeys; use std::{ cmp, @@ -1039,147 +1036,127 @@ impl SqliteConn { } /// Create new Receiver Session - pub fn payjoin_save_receiver_session( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - let session_ser = serde_json::to_vec(&session).unwrap(); + pub fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_receivers (session_id, session) VALUES (?1, ?2)", - rusqlite::params![session_id.0, session_ser], + "INSERT INTO payjoin_receivers (session_id, created_at) VALUES (?1, ?2)", + rusqlite::params![session_id.0, curr_timestamp()], )?; Ok(()) }) .expect("Db must not fail"); } - /// Get a Receiver Session by Id - pub fn payjoin_get_receiver_session( - &mut self, - session_id: &SessionId, - ) -> Option> { + /// Get all active receiver session ids + pub fn get_all_receiver_session_ids(&mut self) -> Vec { db_query( &mut self.conn, - "SELECT session FROM payjoin_receivers WHERE session_id = ?1", - rusqlite::params![session_id.0], + "SELECT session_id FROM payjoin_receivers WHERE completed_at IS NOT NULL", + rusqlite::params![], |row| { - let session_ser: Vec = row.get(0)?; - let session: SessionWrapper = - serde_json::from_slice(&session_ser).unwrap(); - Ok(session) + let id: u64 = row.get(0)?; + Ok(SessionId::new(id)) }, ) .expect("Db must not fail") - .pop() } - pub fn update_payjoin_receiver_status( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - let session_ser = serde_json::to_vec(&session).unwrap(); + /// Save a Receiver Session Event + pub fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_receivers SET session = ?1 WHERE session_id = ?2", - rusqlite::params![session_ser, session_id.0], + "INSERT INTO payjoin_receiver_events (session_id, created_at, event) VALUES (?1, ?2, ?3)", + rusqlite::params![session_id.0, curr_timestamp(), event], )?; Ok(()) }) .expect("Db must not fail"); } - pub fn payjoin_get_all_receiver_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)> { - db_query( - &mut self.conn, - "SELECT session_id, session FROM payjoin_receivers", - rusqlite::params![], - |row| { - let id: u64 = row.get(0)?; - let session_id = SessionId::new(id); - let session_ser: Vec = row.get(1)?; - let session: SessionWrapper = - serde_json::from_slice(&session_ser).unwrap(); - Ok((session_id, session)) - }, - ) - .expect("Db must not fail") - } - - /// Create a payjoin sender - pub fn payjoin_save_sender_session( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - let session_ser = serde_json::to_vec(&session).unwrap(); + /// Update completed at timestamp for a Receiver Session + pub fn update_receiver_session_completed_at(&mut self, session_id: &SessionId) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_senders (session_id, session) VALUES (?1, ?2)", - rusqlite::params![session_id.0, session_ser], + "UPDATE payjoin_receivers SET completed_at = ?1 WHERE session_id = ?2", + rusqlite::params![curr_timestamp(), session_id.0], )?; Ok(()) }) .expect("Db must not fail"); } - pub fn payjoin_get_sender_session( - &mut self, - session_id: &SessionId, - ) -> Option> { + /// Load all receiver session events for a particular session id + pub fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec> { db_query( &mut self.conn, - "SELECT session FROM payjoin_senders WHERE session_id = ?1", + "SELECT event FROM payjoin_receiver_events WHERE session_id = ?1 ORDER BY created_at ASC", rusqlite::params![session_id.0], |row| { - let session_ser: Vec = row.get(0)?; - let session: SessionWrapper = - serde_json::from_slice(&session_ser).unwrap(); - Ok(session) + let event: Vec = row.get(0)?; + Ok(event) }, ) .expect("Db must not fail") - .pop() } - pub fn payjoin_get_all_sender_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)> { + pub fn save_new_payjoin_sender_session(&mut self, session_id: &SessionId) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "INSERT INTO payjoin_senders (session_id, created_at) VALUES (?1, ?2)", + rusqlite::params![session_id.0, curr_timestamp()], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + pub fn get_all_sender_session_ids(&mut self) -> Vec { db_query( &mut self.conn, - "SELECT session_id, session FROM payjoin_senders", + "SELECT session_id FROM payjoin_senders", rusqlite::params![], |row| { let id: u64 = row.get(0)?; - let session_id = SessionId::new(id); - let session_ser: Vec = row.get(1)?; - let session: SessionWrapper = - serde_json::from_slice(&session_ser).unwrap(); - Ok((session_id, session)) + Ok(SessionId::new(id)) }, ) .expect("Db must not fail") } - pub fn update_payjoin_sender_status( - &mut self, - session_id: &SessionId, - session: SessionWrapper, - ) { - let session_ser = serde_json::to_vec(&session).unwrap(); + pub fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "INSERT INTO payjoin_sender_events (session_id, created_at, event) VALUES (?1, ?2, ?3)", + rusqlite::params![session_id.0, curr_timestamp(), event], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + pub fn update_sender_session_completed_at(&mut self, session_id: &SessionId) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_senders SET session = ?1 WHERE session_id = ?2", - rusqlite::params![session_ser, session_id.0], + "UPDATE payjoin_senders SET completed_at = ?1 WHERE session_id = ?2", + rusqlite::params![curr_timestamp(), session_id.0], )?; Ok(()) }) .expect("Db must not fail"); } + + pub fn load_sender_session_events(&mut self, session_id: &SessionId) -> Vec> { + db_query( + &mut self.conn, + "SELECT event FROM payjoin_sender_events WHERE session_id = ?1 ORDER BY created_at ASC", + rusqlite::params![session_id.0], + |row| { + let event: Vec = row.get(0)?; + Ok(event) + }, + ) + .expect("Db must not fail") + } } #[cfg(test)] diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 0da66aea4..edec937dd 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -135,14 +135,34 @@ CREATE TABLE payjoin_ohttp_keys ( CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, session_id INTEGER UNIQUE NOT NULL, - session BLOB NOT NULL + created_at INTEGER NOT NULL, + completed_at INTEGER +); + +/* Payjoin Sender session events */ +CREATE TABLE payjoin_sender_events ( + id INTEGER PRIMARY KEY NOT NULL, + session_id INTEGER NOT NULL, + created_at INTEGER NOT NULL, + event BLOB NOT NULL, + FOREIGN KEY (session_id) REFERENCES payjoin_senders (session_id) ); /* Payjoin receivers */ CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, session_id INTEGER UNIQUE NOT NULL, - session BLOB NOT NULL + created_at INTEGER NOT NULL, + completed_at INTEGER +); + +/* Payjoin Receiver session events */ +CREATE TABLE payjoin_receiver_events ( + id INTEGER PRIMARY KEY NOT NULL, + session_id INTEGER NOT NULL, + created_at INTEGER NOT NULL, + event BLOB NOT NULL, + FOREIGN KEY (session_id) REFERENCES payjoin_receivers (session_id) ); "; diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 1b3cdd2c6..a4f04fe66 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -1,31 +1,12 @@ -use payjoin::bitcoin::{Psbt, Txid}; use payjoin::persist::SessionPersister; use payjoin::receive::v2::SessionEvent as ReceiverSessionEvent; use payjoin::send::v2::SessionEvent as SenderSessionEvent; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display, Formatter}; use std::sync::Arc; -use std::time::SystemTime; use crate::database::DatabaseInterface; -use super::types::PayjoinStatus; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionMetadata { - pub status: PayjoinStatus, - pub maybe_txid: Option, - pub maybe_psbt: Option, - pub maybe_bip21: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionWrapper { - pub metadata: SessionMetadata, - pub events: Vec, - pub completed_at: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionId(pub u64); @@ -64,42 +45,13 @@ impl ReceiverPersister { pub fn new(db: Arc) -> Self { let mut db_conn = db.connection(); let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_receivers")); - let session: SessionWrapper = SessionWrapper { - metadata: SessionMetadata { - status: PayjoinStatus::Pending, - maybe_txid: None, - maybe_psbt: None, - maybe_bip21: None, - }, - events: vec![], - completed_at: None, - }; - db_conn.payjoin_save_receiver_session(&session_id, session); + db_conn.save_new_payjoin_receiver_session(&session_id); Self { db, session_id } } pub fn from_id(db: Arc, id: SessionId) -> Self { Self { db, session_id: id } } - - pub fn update_metadata( - &self, - status: Option, - maybe_txid: Option, - maybe_psbt: Option, - maybe_bip21: Option, - ) { - let mut db_conn = self.db.connection(); - if let Some(mut session) = db_conn.payjoin_get_receiver_session(&self.session_id) { - session.metadata = SessionMetadata { - status: status.unwrap_or(PayjoinStatus::Pending), - maybe_txid, - maybe_psbt, - maybe_bip21, - }; - db_conn.update_payjoin_receiver_status(&self.session_id, session); - } - } } impl SessionPersister for ReceiverPersister { @@ -111,12 +63,9 @@ impl SessionPersister for ReceiverPersister { event: &Self::SessionEvent, ) -> std::result::Result<(), Self::InternalStorageError> { let mut db_conn = self.db.connection(); - // Check if key exists - if let Some(mut session) = db_conn.payjoin_get_receiver_session(&self.session_id) { - // Append new event - session.events.push(event.clone()); - db_conn.update_payjoin_receiver_status(&self.session_id, session); - } + // serilize event + let event_ser = serde_json::to_vec(event).map_err(PersisterError::Serialize)?; + db_conn.save_receiver_session_event(&self.session_id, event_ser); Ok(()) } @@ -125,18 +74,17 @@ impl SessionPersister for ReceiverPersister { ) -> std::result::Result>, Self::InternalStorageError> { let mut db_conn = self.db.connection(); - let session = db_conn - .payjoin_get_receiver_session(&self.session_id) - .expect("key should exist"); - Ok(Box::new(session.events.into_iter())) + let events = db_conn.load_receiver_session_events(&self.session_id); + let deserialized_events: Result, _> = events + .into_iter() + .map(|event| serde_json::from_slice(&event).map_err(PersisterError::Deserialize)) + .collect(); + Ok(Box::new(deserialized_events?.into_iter())) } fn close(&self) -> std::result::Result<(), Self::InternalStorageError> { let mut db_conn = self.db.connection(); - if let Some(mut session) = db_conn.payjoin_get_receiver_session(&self.session_id) { - session.completed_at = Some(SystemTime::now()); - db_conn.update_payjoin_receiver_status(&self.session_id, session); - } + db_conn.update_receiver_session_completed_at(&self.session_id); Ok(()) } } @@ -151,42 +99,13 @@ impl SenderPersister { pub fn new(db: Arc) -> Self { let mut db_conn = db.connection(); let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_senders")); - let session: SessionWrapper = SessionWrapper { - metadata: SessionMetadata { - status: PayjoinStatus::Pending, - maybe_txid: None, - maybe_psbt: None, - maybe_bip21: None, - }, - events: vec![], - completed_at: None, - }; - db_conn.payjoin_save_sender_session(&session_id, session); + db_conn.save_new_payjoin_sender_session(&session_id); Self { db, session_id } } pub fn from_id(db: Arc, id: SessionId) -> Self { Self { db, session_id: id } } - - pub fn update_metadata( - &self, - status: Option, - maybe_txid: Option, - maybe_psbt: Option, - maybe_bip21: Option, - ) { - let mut db_conn = self.db.connection(); - if let Some(mut session) = db_conn.payjoin_get_sender_session(&self.session_id) { - session.metadata = SessionMetadata { - status: status.unwrap_or(PayjoinStatus::Pending), - maybe_txid, - maybe_psbt, - maybe_bip21, - }; - db_conn.update_payjoin_sender_status(&self.session_id, session); - } - } } impl SessionPersister for SenderPersister { @@ -198,12 +117,9 @@ impl SessionPersister for SenderPersister { event: &Self::SessionEvent, ) -> std::result::Result<(), Self::InternalStorageError> { let mut db_conn = self.db.connection(); - // Check if key exists - if let Some(mut session) = db_conn.payjoin_get_sender_session(&self.session_id) { - // Append new event - session.events.push(event.clone()); - db_conn.update_payjoin_sender_status(&self.session_id, session); - } + // serilize event + let event_ser = serde_json::to_vec(event).map_err(PersisterError::Serialize)?; + db_conn.save_sender_session_event(&self.session_id, event_ser); Ok(()) } @@ -212,18 +128,17 @@ impl SessionPersister for SenderPersister { ) -> std::result::Result>, Self::InternalStorageError> { let mut db_conn = self.db.connection(); - let session = db_conn - .payjoin_get_sender_session(&self.session_id) - .expect("key should exist"); - Ok(Box::new(session.events.into_iter())) + let events = db_conn.get_all_sender_session_events(&self.session_id); + let deserialized_events: Result, _> = events + .into_iter() + .map(|event| serde_json::from_slice(&event).map_err(PersisterError::Deserialize)) + .collect(); + Ok(Box::new(deserialized_events?.into_iter())) } fn close(&self) -> std::result::Result<(), Self::InternalStorageError> { let mut db_conn = self.db.connection(); - if let Some(mut session) = db_conn.payjoin_get_sender_session(&self.session_id) { - session.completed_at = Some(SystemTime::now()); - db_conn.update_payjoin_sender_status(&self.session_id, session); - } + db_conn.update_sender_session_completed_at(&self.session_id); Ok(()) } } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index be8a566bb..ec14bf994 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, error::Error, sync::{self, Arc}, - time::SystemTime, }; use liana::descriptors; @@ -15,8 +14,8 @@ use payjoin::{ receive::{ v2::{ replay_event_log, Initialized, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, - PayjoinProposal, ProvisionalProposal, ReceiveSession, Receiver, SessionHistory, - UncheckedProposal, WantsInputs, WantsOutputs, + PayjoinProposal, ProvisionalProposal, ReceiveSession, Receiver, UncheckedProposal, + WantsInputs, WantsOutputs, }, InputPair, }, @@ -28,7 +27,7 @@ use crate::{ payjoin::helpers::{finalize_psbt, post_request, OHTTP_RELAY}, }; -use super::{db::ReceiverPersister, types::PayjoinStatus}; +use super::db::ReceiverPersister; fn read_from_directory( receiver: Receiver, @@ -195,28 +194,22 @@ fn contribute_inputs( .unwrap(); let psbt = history.psbt_with_contributed_inputs().unwrap(); - let bip21 = history.pj_uri().unwrap().to_string(); db_conn.store_spend(&psbt); log::info!("[Payjoin] PSBT in the DB..."); - persister.update_metadata( - Some(PayjoinStatus::Signing), - Some(psbt.unsigned_tx.compute_txid()), - Some(psbt.clone()), - Some(bip21.clone()), - ); - Ok(()) } fn finalize_proposal( proposal: Receiver, persister: &ReceiverPersister, - history: SessionHistory, db_conn: &mut Box, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { + let (_, history) = replay_event_log(persister) + .map_err(|e| format!("Failed to replay receiver event log: {:?}", e))?; + if let Some(proposed_psbt) = history.psbt_with_contributed_inputs() { let txid = proposed_psbt.unsigned_tx.compute_txid(); if let Some(psbt) = db_conn.spend_tx(&txid) { @@ -242,7 +235,7 @@ fn finalize_proposal( ) .save(persister)?; - send_payjoin_proposal(proposal, persister, history)?; + send_payjoin_proposal(proposal, persister)?; } } } @@ -252,15 +245,11 @@ fn finalize_proposal( fn send_payjoin_proposal( mut proposal: Receiver, persister: &ReceiverPersister, - history: SessionHistory, ) -> Result<(), Box> { let (req, ctx) = proposal .extract_req(OHTTP_RELAY) .expect("Failed to extract request"); - let psbt = proposal.psbt().clone(); - let txid = psbt.unsigned_tx.compute_txid(); - // Respond to sender log::info!("[Payjoin] Receiver responding to sender..."); match post_request(req) { @@ -268,14 +257,6 @@ fn send_payjoin_proposal( proposal .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) .save(persister)?; - - let bip21 = history.pj_uri().unwrap(); - persister.update_metadata( - Some(PayjoinStatus::Completed), - Some(txid), - Some(psbt), - Some(bip21.to_string()), - ); } Err(err) => log::error!("[Payjoin] send_payjoin_proposal(): {}", err), } @@ -289,17 +270,10 @@ fn process_receiver_session( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let mut db_conn = db.connection(); - for (session_id, session) in db_conn.payjoin_get_all_receiver_sessions() { - // No need to check past completed payjoins - if session.completed_at.is_some() - && SystemTime::now() > session.completed_at.expect("Some system time exists") - { - continue; - } - + for session_id in db_conn.get_all_receiver_session_ids() { let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()); - let (state, history) = replay_event_log(&persister) + let (state, _) = replay_event_log(&persister) .map_err(|e| format!("Failed to replay receiver event log: {:?}", e))?; match state { @@ -325,10 +299,10 @@ fn process_receiver_session( contribute_inputs(proposal, &persister, &mut db_conn, desc, secp)? } ReceiveSession::ProvisionalProposal(proposal) => { - finalize_proposal(proposal, &persister, history, &mut db_conn, secp)? + finalize_proposal(proposal, &persister, &mut db_conn, secp)? } ReceiveSession::PayjoinProposal(proposal) => { - send_payjoin_proposal(proposal, &persister, history)? + send_payjoin_proposal(proposal, &persister)? } _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), } diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 3afb500df..fbac2e881 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -1,6 +1,5 @@ use crate::database::DatabaseInterface; -use crate::payjoin::db::SessionMetadata; use crate::payjoin::helpers::post_request; use std::collections::HashMap; @@ -14,7 +13,6 @@ use payjoin::send::v2::{Sender, WithReplyKey}; use super::db::SenderPersister; use super::helpers::OHTTP_RELAY; -use super::types::PayjoinStatus; fn get_proposed_payjoin_psbt( context: Sender, @@ -93,85 +91,63 @@ fn process_sender_session( pub fn payjoin_sender_check(db: &sync::Arc>) { let mut db_conn = db.connection(); - for (session_id, session) in db_conn.payjoin_get_all_sender_sessions() { - let SessionMetadata { - status, - maybe_txid, - maybe_psbt, - maybe_bip21, - } = session.metadata.clone(); - - // No need to check Completed - if status == PayjoinStatus::Completed { - continue; - } - - log::info!("[Payjoin] {:?}: bip21={:?}", status, maybe_bip21); - + for session_id in db_conn.get_all_sender_session_ids() { let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()); - let (state, _) = replay_event_log(&persister) + let (state, session_history) = replay_event_log(&persister) .map_err(|e| format!("Failed to replay sender event log: {:?}", e)) + // TODO: handle error .unwrap(); - - match status { - PayjoinStatus::Pending => match process_sender_session(state, &persister) { - Ok(_) => persister.update_metadata( - Some(PayjoinStatus::WaitingReceiver), - maybe_txid, - maybe_psbt, - maybe_bip21, - ), - Err(e) => log::warn!("process_sender_session(): {}", e), - }, - PayjoinStatus::WaitingReceiver => match process_sender_session(state, &persister) { - Ok(maybe_psbt) => { - if let Some(mut new_psbt) = maybe_psbt { - if let Some(txid) = maybe_txid { - if let Some(psbt) = db_conn.spend_tx(&txid) { - // TODO(arturgontijo): PDK removes fields that we need in the GUI to properly sign the inputs - let mut input_fields_to_restore = HashMap::new(); - for (index, txin) in psbt.unsigned_tx.input.iter().enumerate() { - let mut input_without_sigs = psbt.inputs[index].clone(); - input_without_sigs.partial_sigs = Default::default(); - input_fields_to_restore - .insert(txin.previous_output, input_without_sigs); - } - log::info!( - "[Payjoin] Deleting original Payjoin psbt (txid={txid})" - ); - db_conn.delete_spend(&txid); - - // TODO(arturgontijo): Restoring witness_scripts and bip32_derivation so GUI can sign them - for (index, psbtin) in new_psbt.inputs.iter_mut().enumerate() { - let outpoint = - &new_psbt.unsigned_tx.input[index].previous_output; - if let Some(input) = input_fields_to_restore.get(outpoint) { - *psbtin = input.clone(); - } - } - - let new_txid = new_psbt.unsigned_tx.compute_txid(); - log::info!( - "[Payjoin] Updating Payjoin psbt: {} -> {}", - txid, - new_txid, - ); - db_conn.store_spend(&new_psbt); - - persister.update_metadata( - Some(PayjoinStatus::Completed), - Some(new_txid), - Some(new_psbt), - maybe_bip21, - ); - } - } + let original_psbt = match session_history.fallback_tx().map(|tx| tx.compute_txid()) { + Some(txid) => { + // Get the original psbt so we can restore the input fields + let original_psbt = db_conn.spend_tx(&txid); + if original_psbt.is_none() { + log::error!("[Payjoin] expecting fallback txid for session={session_id:?}, but none found"); + return; + } + original_psbt.expect("checked above") + } + None => { + log::info!("[Payjoin] No fallback txid found for session={session_id:?}"); + return; + } + }; + + match process_sender_session(state, &persister) { + Ok(Some(proposal_psbt)) => { + let mut proposal_psbt = proposal_psbt; + // TODO(arturgontijo): PDK removes fields that we need in the GUI to properly sign the inputs + let mut input_fields_to_restore = HashMap::new(); + for (index, txin) in original_psbt.unsigned_tx.input.iter().enumerate() { + let mut input_without_sigs = original_psbt.inputs[index].clone(); + input_without_sigs.partial_sigs = Default::default(); + input_fields_to_restore.insert(txin.previous_output, input_without_sigs); + } + let original_txid = original_psbt.unsigned_tx.compute_txid(); + log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); + db_conn.delete_spend(&original_txid); + + // TODO(arturgontijo): Restoring witness_scripts and bip32_derivation so GUI can sign them + for (index, psbtin) in proposal_psbt.inputs.iter_mut().enumerate() { + let outpoint = &proposal_psbt.unsigned_tx.input[index].previous_output; + if let Some(input) = input_fields_to_restore.get(outpoint) { + *psbtin = input.clone(); } } - Err(e) => log::warn!("payjoin_sender_check(): {}", e), - }, - _ => {} + + let new_txid = proposal_psbt.unsigned_tx.compute_txid(); + log::info!( + "[Payjoin] Updating Payjoin psbt: {} -> {}", + original_txid, + new_txid + ); + db_conn.store_spend(&proposal_psbt); + } + Ok(None) => { + log::info!("[Payjoin] Proposal not received yet..."); + } + Err(e) => log::warn!("payjoin_sender_check(): {}", e), } } } diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 5deec016b..cd71cf10c 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -5,7 +5,7 @@ use crate::{ BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem, Wallet, }, datadir::DataDirectory, - payjoin::db::{SessionId, SessionWrapper}, + payjoin::db::SessionId, DaemonControl, DaemonHandle, }; use liana::descriptors; @@ -560,73 +560,54 @@ impl DatabaseConnection for DummyDatabase { todo!() } - fn payjoin_get_all_receiver_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)> { + fn get_all_receiver_session_ids(&mut self) -> Vec { + todo!() + } + fn save_new_payjoin_sender_session(&mut self, _session_id: &SessionId) { todo!() } - fn update_payjoin_receiver_status( - &mut self, - _session_id: &SessionId, - _session: SessionWrapper, - ) { + fn payjoin_next_id(&mut self, _table: &str) -> u64 { todo!() } - fn payjoin_save_sender_session( - &mut self, - _session_id: &SessionId, - _session: SessionWrapper, - ) { + fn get_all_sender_session_ids(&mut self) -> Vec { todo!() } - fn update_payjoin_sender_status( - &mut self, - _session_id: &SessionId, - _session: SessionWrapper, - ) { + fn payjoin_get_ohttp_keys(&mut self, _ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { todo!() } - fn payjoin_next_id(&mut self, _table: &str) -> u64 { + fn payjoin_save_ohttp_keys(&mut self, _ohttp_relay: &str, _ohttp_keys: payjoin::OhttpKeys) { todo!() } - fn payjoin_save_receiver_session( - &mut self, - _session_id: &SessionId, - _session: SessionWrapper, - ) { + fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId) { todo!() } - fn payjoin_get_receiver_session( - &mut self, - _session_id: &SessionId, - ) -> Option> { + fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec) { todo!() } - fn payjoin_get_sender_session( - &mut self, - _session_id: &SessionId, - ) -> Option> { + fn update_receiver_session_completed_at(&mut self, session_id: &SessionId) { todo!() } - fn payjoin_get_all_sender_sessions( - &mut self, - ) -> Vec<(SessionId, SessionWrapper)> { + fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec> { todo!() } - fn payjoin_get_ohttp_keys(&mut self, _ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { + fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec) { todo!() } - fn payjoin_save_ohttp_keys(&mut self, _ohttp_relay: &str, _ohttp_keys: payjoin::OhttpKeys) { + fn get_all_sender_session_events(&mut self, session_id: &SessionId) -> Vec> { + todo!() + } + + fn update_sender_session_completed_at(&mut self, session_id: &SessionId) { todo!() } } From c9ba5cbe635e917b8f5a56f61cc3bcf4fd8f9a42 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 14:39:24 -0400 Subject: [PATCH 25/72] Remove unused pj json-rpc endpoints --- lianad/src/commands/mod.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index f865e13da..12b3a0395 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -9,7 +9,7 @@ use crate::{ database::{Coin, DatabaseConnection, DatabaseInterface}, miniscript::bitcoin::absolute::LockTime, payjoin::{ - db::{ReceiverPersister, SenderPersister, SessionId}, + db::{ReceiverPersister, SenderPersister}, helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, }, poller::PollerMessage, @@ -455,17 +455,6 @@ impl DaemonControl { Ok(()) } - // TODO: this seems unsued can we remove? - pub fn payjoin_get_all_receiver_sessions(&self) -> Vec { - let mut db_conn = self.db.connection(); - db_conn.get_all_receiver_session_ids() - } - - // TODO: is this unused? Can we remove it? - pub fn payjoin_get_receiver_session(&self, session_id: &SessionId) -> ReceiverPersister { - ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()) - } - /// Update derivation indexes pub fn update_deriv_indexes( &self, From 1e358b31bfa190479a05401e062753285f238b59 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 9 Jul 2025 14:59:33 -0400 Subject: [PATCH 26/72] Revert "Remove get payjoin info endpoint" This reverts commit 357138b2b2c6649fe64691d4a1c505ee47a278c9. --- liana-gui/src/daemon/client/mod.rs | 5 ++ liana-gui/src/daemon/embedded.rs | 10 ++++ liana-gui/src/daemon/mod.rs | 12 ++--- .../services/connect/client/backend/mod.rs | 5 ++ lianad/src/commands/mod.rs | 54 ++++++++++++++++++- lianad/src/jsonrpc/api.rs | 18 +++++++ lianad/src/payjoin/types.rs | 49 +++++++++++------ 7 files changed, 129 insertions(+), 24 deletions(-) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index ecbf12791..50cd30c71 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,6 +5,7 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; +use lianad::payjoin::types::PayjoinInfo; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -117,6 +118,10 @@ impl Daemon for Lianad { Ok(()) } + async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { + self.call("getpayjoininfo", Some(vec![txid.to_string()])) + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index e0f52b841..4170fccfb 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,5 +1,6 @@ use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; +use lianad::payjoin::types::PayjoinInfo; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; @@ -138,6 +139,15 @@ impl Daemon for EmbeddedDaemon { .await } + async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { + self.command(|daemon| { + daemon + .get_payjoin_info(txid) + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await + } + async fn update_deriv_indexes( &self, receive: Option, diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 960ea1d80..ef7c00e80 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -18,6 +18,7 @@ use liana::miniscript::bitcoin::{ }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; +use lianad::payjoin::types::PayjoinInfo; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -116,6 +117,7 @@ pub trait Daemon: Debug { ) -> Result; async fn receive_payjoin(&self) -> Result; async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; + async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError>; async fn update_deriv_indexes( &self, receive: Option, @@ -210,11 +212,9 @@ pub trait Daemon: Debug { .cloned() .collect(); - // TODO: get payjoin info if a session exists for this transaction - // Status should be derived from replaying the session - // let payjoin_info = self - // .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) - // .await?; + let payjoin_info = self + .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) + .await?; spend_txs.push(model::SpendTx::new( tx.updated_at, @@ -223,7 +223,7 @@ pub trait Daemon: Debug { &info.descriptors.main, &curve, info.network, - None, // Payjoin status + payjoin_info, )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index 45d10b620..f16dfc339 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -17,6 +17,7 @@ use lianad::{ bip329::Labels, commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, + payjoin::types::PayjoinInfo, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; @@ -631,6 +632,10 @@ impl Daemon for BackendWalletClient { unimplemented!() } + async fn get_payjoin_info(&self, _txid: &Txid) -> Result, DaemonError> { + unimplemented!() + } + async fn list_revealed_addresses( &self, is_change: bool, diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 12b3a0395..4dd31f8e0 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -11,6 +11,7 @@ use crate::{ payjoin::{ db::{ReceiverPersister, SenderPersister}, helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, + types::PayjoinInfo, }, poller::PollerMessage, DaemonControl, VERSION, @@ -49,8 +50,8 @@ use miniscript::{ }; use payjoin::{ bitcoin::{key::Secp256k1, FeeRate}, - receive::v2::{Receiver, UninitializedReceiver}, - send::v2::SenderBuilder, + receive::v2::{replay_event_log as replay_receiver_event_log, Receiver, UninitializedReceiver}, + send::v2::{replay_event_log as replay_sender_event_log, SenderBuilder}, Uri, UriExt, }; use serde::{Deserialize, Serialize}; @@ -455,6 +456,55 @@ impl DaemonControl { Ok(()) } + /// Get Payjoin URI (BIP21) and its sender/receiver status by txid + /// TODO: this seems unused, can we remove it? + pub fn get_payjoin_info( + &self, + txid: &bitcoin::Txid, + ) -> Result, CommandError> { + let mut db_conn = self.db.connection(); + + for session_id in db_conn.get_all_receiver_session_ids() { + let persister = + ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); + let (state, history) = replay_receiver_event_log(&persister).unwrap(); + let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); + if let Some(original_txid) = original_txid { + if original_txid == *txid { + let bip21 = history + .pj_uri() + .expect("should exist at this point") + .to_string(); + return Ok(Some(PayjoinInfo { + bip21, + status: state.into(), + })); + } + } + } + + for session_id in db_conn.get_all_sender_session_ids() { + let persister = SenderPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); + let (state, history) = replay_sender_event_log(&persister).unwrap(); + let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); + if let Some(original_txid) = original_txid { + if original_txid == *txid { + // TODO: this isnt a bip21, but a payjoin endpoint. Does this need to get returned? + let bip21 = history + .endpoint() + .expect("should exist at this point") + .to_string(); + return Ok(Some(PayjoinInfo { + bip21, + status: state.into(), + })); + } + } + } + + Ok(None) + } + /// Update derivation indexes pub fn update_deriv_indexes( &self, diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index dce46a5fe..a4196f974 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -508,6 +508,18 @@ fn send_payjoin(control: &DaemonControl, params: Params) -> Result Result { + let txid = params + .get(0, "txid") + .ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))? + .as_str() + .ok_or_else(|| Error::invalid_params("Invalid 'txid' parameter."))?; + let txid = bitcoin::Txid::from_str(txid) + .map_err(|_| Error::invalid_params("Invalid 'txid' parameter."))?; + let res = control.get_payjoin_info(&txid)?; + Ok(serde_json::json!(&res)) +} + /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -622,6 +634,12 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { + let params = req + .params + .ok_or_else(|| Error::invalid_params("Missing 'txid' parameter."))?; + get_payjoin_info(control, params)? + } _ => { return Err(Error::method_not_found()); } diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index 4e3a8e6cb..78c46a91f 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -1,30 +1,47 @@ +use payjoin::{receive::v2::ReceiveSession, send::v2::SendSession}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PayjoinStatus { - Pending = 0, - WaitingReceiver = 1, - Signing = 2, - Completed = 3, - // TODO: more specific enums for why it failed - Failed = 4, + Pending, + WaitingToSign, + Success, + Failed, } -impl From for PayjoinStatus { - fn from(status: i32) -> Self { - match status { - 0 => PayjoinStatus::Pending, - 1 => PayjoinStatus::WaitingReceiver, - 2 => PayjoinStatus::Signing, - 3 => PayjoinStatus::Completed, - 4 => PayjoinStatus::Failed, - _ => panic!("Invalid payjoin status: {}", status), +impl From for PayjoinStatus { + fn from(session: ReceiveSession) -> Self { + match session { + ReceiveSession::Uninitialized(_) + | ReceiveSession::Initialized(_) + | ReceiveSession::UncheckedProposal(_) + | ReceiveSession::MaybeInputsOwned(_) + | ReceiveSession::MaybeInputsSeen(_) + | ReceiveSession::OutputsUnknown(_) + | ReceiveSession::WantsOutputs(_) + | ReceiveSession::WantsInputs(_) => PayjoinStatus::Pending, + ReceiveSession::ProvisionalProposal(_) => PayjoinStatus::WaitingToSign, + ReceiveSession::PayjoinProposal(_) => PayjoinStatus::Success, + ReceiveSession::TerminalFailure => PayjoinStatus::Failed, + } + } +} + +// TODO: None of the current states lead to a successful status +impl From for PayjoinStatus { + fn from(session: SendSession) -> Self { + match session { + SendSession::Uninitialized + | SendSession::WithReplyKey(_) + | SendSession::V2GetContext(_) => PayjoinStatus::Pending, + SendSession::ProposalReceived(_) => PayjoinStatus::WaitingToSign, + SendSession::TerminalFailure => PayjoinStatus::Failed, } } } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PayjoinInfo { - pub status: Option, + pub status: PayjoinStatus, pub bip21: String, } From abf9c87f513154a44c7a7a288e9c0006ea7f6034 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 10 Jul 2025 08:22:03 -0400 Subject: [PATCH 27/72] Do update existing pj proposal --- lianad/src/payjoin/sender.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index fbac2e881..761ff60f6 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -125,6 +125,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) input_fields_to_restore.insert(txin.previous_output, input_without_sigs); } let original_txid = original_psbt.unsigned_tx.compute_txid(); + // TODO: should we be deleting the original psbt? can we fallback without it? log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); db_conn.delete_spend(&original_txid); @@ -136,7 +137,12 @@ pub fn payjoin_sender_check(db: &sync::Arc>) } } + // TODO: need a check here to see if this proposal already exists in the db let new_txid = proposal_psbt.unsigned_tx.compute_txid(); + if db_conn.spend_tx(&new_txid).is_some() { + log::info!("[Payjoin] Proposal already exists in the db"); + return; + } log::info!( "[Payjoin] Updating Payjoin psbt: {} -> {}", original_txid, From cf9ad7766986aac8d8eb3eac64748cbcb40ac1e9 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 10 Jul 2025 10:01:59 -0400 Subject: [PATCH 28/72] Fix getting active payjoins sessions --- lianad/src/database/sqlite/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index adda9e229..d6872fe87 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -1051,7 +1051,7 @@ impl SqliteConn { pub fn get_all_receiver_session_ids(&mut self) -> Vec { db_query( &mut self.conn, - "SELECT session_id FROM payjoin_receivers WHERE completed_at IS NOT NULL", + "SELECT session_id FROM payjoin_receivers WHERE completed_at IS NULL", rusqlite::params![], |row| { let id: u64 = row.get(0)?; @@ -1113,7 +1113,7 @@ impl SqliteConn { pub fn get_all_sender_session_ids(&mut self) -> Vec { db_query( &mut self.conn, - "SELECT session_id FROM payjoin_senders", + "SELECT session_id FROM payjoin_senders WHERE completed_at IS NULL", rusqlite::params![], |row| { let id: u64 = row.get(0)?; From ed22c01b6bd76e9921f3ded990d3a72181430d41 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 10 Jul 2025 10:03:21 -0400 Subject: [PATCH 29/72] gui: path is ready if all owned inputs are signed --- liana-gui/src/app/state/psbt.rs | 8 -------- liana-gui/src/app/state/spend/step.rs | 8 ++++++-- liana-gui/src/app/view/psbt.rs | 3 ++- liana-gui/src/daemon/model.rs | 23 +++++++++-------------- 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index a4fdcc30d..9413251b5 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -12,7 +12,6 @@ use lianad::commands::CoinStatus; use liana_ui::component::toast; use liana_ui::{component::modal, widget::Element}; -use lianad::payjoin::types::PayjoinStatus; use crate::daemon::model::LabelsLoader; use crate::export::{ImportExportMessage, ImportExportType, Progress}; @@ -586,13 +585,6 @@ impl Modal for SignModal { let daemon = daemon.clone(); merge_signatures(&mut tx.psbt, &psbt); - // TODO(arturgontijo): Use better design. Maybe checking for foreign inputs. - if let Some(payjoin_info) = &tx.payjoin_info { - if payjoin_info.status == Some(PayjoinStatus::Signing) { - tx.status = SpendStatus::PayjoinProposalReady; - } - } - if self.is_saved { return Task::perform( async move { daemon.update_spend_tx(&psbt).await.map_err(|e| e.into()) }, diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index be40c8307..4744c75c8 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -18,7 +18,10 @@ use liana::{ }, spend::{SpendCreationError, MAX_FEERATE}, }; -use lianad::{commands::ListCoinsEntry, payjoin::types::PayjoinInfo}; +use lianad::{ + commands::ListCoinsEntry, + payjoin::types::{PayjoinInfo, PayjoinStatus}, +}; use liana_ui::{component::form, widget::Element}; use payjoin::Uri; @@ -992,9 +995,10 @@ impl Step for SaveSpend { let payjoin_info = if bip21.is_empty() { None } else { + // TODO: this seems wrong. The presence of a bip21 doesnt mean its a payjoin Some(PayjoinInfo { bip21, - status: None, + status: PayjoinStatus::Pending, }) }; diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index 97746bee0..824773d43 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -25,6 +25,7 @@ use liana_ui::{ icon, theme, widget::*, }; +use lianad::payjoin::types::PayjoinStatus; use crate::{ app::{ @@ -424,7 +425,7 @@ pub fn spend_overview_view<'a>( }) .push_maybe(if tx.path_ready().is_some() { if let Some(payjoin_info) = &tx.payjoin_info { - if payjoin_info.status.is_none() { + if payjoin_info.status == PayjoinStatus::Pending { Some( button::secondary(None, "Send Payjoin") .on_press(Message::Spend(SpendTxMessage::SendPayjoin)) diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 57fcc4e26..431e12205 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -15,7 +15,7 @@ pub use lianad::commands::{ ListCoinsResult, ListRevealedAddressesEntry, ListRevealedAddressesResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, }; -use lianad::payjoin::types::{PayjoinInfo, PayjoinStatus}; +use lianad::payjoin::types::PayjoinInfo; pub type Coin = ListCoinsEntry; @@ -203,19 +203,14 @@ impl SpendTx { pub fn path_ready(&self) -> Option<&PathSpendInfo> { let path = self.sigs.primary_path(); - // TODO(arturgontijo): We should count the sigs, just in case. - if let Some(payjoin_info) = &self.payjoin_info { - if let Some(PayjoinStatus::Completed) = payjoin_info.status { - let has_sigs = self - .psbt - .inputs - .iter() - .any(|psbtin| !psbtin.partial_sigs.is_empty()); - if has_sigs { - return Some(path); - } - } - }; + // Check if we have signatures for all of our inputs + let has_sigs = + self.psbt.inputs.iter().any(|psbtin| { + !psbtin.partial_sigs.is_empty() && !psbtin.bip32_derivation.is_empty() + }); + if has_sigs { + return Some(path); + } if path.sigs_count >= path.threshold { return Some(path); From 04a75119ba926847084de860c9b3b0bbda7c2fc6 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 11 Jul 2025 09:25:37 -0400 Subject: [PATCH 30/72] Add expected weight to inputs --- Cargo.lock | 6 +-- liana-gui/Cargo.toml | 2 +- lianad/Cargo.toml | 2 +- lianad/src/payjoin/receiver.rs | 87 +++++++++++++++++++--------------- 4 files changed, 54 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68d97edd0..b505ae80f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,7 +4285,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.24.0" -source = "git+https://github.com/arminsabouri/rust-payjoin.git?branch=use-witness-for-expected-weight#2901576ddf73a5720f8aa0a7632201f196db9cd0" +source = "git+https://github.com/arminsabouri/rust-payjoin.git?branch=pub-apply-fee#4f7c7015f08778353641f661bfed9982626d3979" dependencies = [ "bhttp", "bitcoin", @@ -5323,7 +5323,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.12.0", + "bitcoin_hashes 0.13.0", "rand 0.8.5", "secp256k1-sys", "serde", @@ -6839,7 +6839,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index 93745ef09..a59b06770 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,7 +55,7 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "use-witness-for-expected-weight", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "pub-apply-fee", features = ["v2", "io"] } [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index dcd9c040e..3b0f9a75f 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,5 +60,5 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "use-witness-for-expected-weight", features = ["v2", "io"] } +payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "pub-apply-fee", features = ["v2", "io"] } reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index ec14bf994..6337d665d 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -8,7 +8,8 @@ use liana::descriptors; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, OutPoint, Sequence, TxIn, Weight, + consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, + TxIn, Weight, }, persist::OptionalTransitionOutcome, receive::{ @@ -171,29 +172,42 @@ fn contribute_inputs( }; derived_desc.update_psbt_in(&mut psbtin); - let worse_case_weight = Weight::from_wu_usize(desc.max_sat_weight(true)); + // TODO: revisit using primary path boolean. Perphaps we should use both paths and take the max. + let worse_case_weight = Weight::from_wu_usize(desc.max_sat_weight(true)) + // Segwit marker + + Weight::from_wu(2) + // Non-witness data size + + Weight::from_non_witness_data_size(txin.base_size() as u64); candidate_inputs_map.insert(*outpoint, (*coin, txin, psbtin, worse_case_weight)); } - let candidate_inputs = candidate_inputs_map + let mut candidate_inputs = candidate_inputs_map .values() .map(|(_, txin, psbtin, weight)| { InputPair::new(txin.clone(), psbtin.clone(), Some(*weight)).unwrap() }); + log::info!("[Payjoin] Candidate inputs: {:?}", candidate_inputs); - let selected_input = proposal.try_preserving_privacy(candidate_inputs).unwrap(); + if candidate_inputs.len() == 0 { + return Err("No candidate inputs".into()); + } + + let selected_input = proposal + .try_preserving_privacy(candidate_inputs.clone()) + .unwrap_or( + candidate_inputs + .next() + .expect("Should have at least one input") + .clone(), + ); - proposal + let mut proposal = proposal .contribute_inputs(vec![selected_input])? .commit_inputs() .save(persister)?; - let (_, history) = replay_event_log(persister) - .map_err(|e| format!("Failed to replay receiver event log: {:?}", e)) - .unwrap(); - - let psbt = history.psbt_with_contributed_inputs().unwrap(); + let psbt = proposal.apply_fee(None, None).unwrap(); db_conn.store_spend(&psbt); log::info!("[Payjoin] PSBT in the DB..."); @@ -207,36 +221,33 @@ fn finalize_proposal( db_conn: &mut Box, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { - let (_, history) = replay_event_log(persister) - .map_err(|e| format!("Failed to replay receiver event log: {:?}", e))?; - - if let Some(proposed_psbt) = history.psbt_with_contributed_inputs() { - let txid = proposed_psbt.unsigned_tx.compute_txid(); - if let Some(psbt) = db_conn.spend_tx(&txid) { - let mut is_signed = false; - for psbtin in &psbt.inputs { - if !psbtin.partial_sigs.is_empty() { - log::debug!("[Payjoin] PSBT is signed!"); - is_signed = true; - break; - } + let mut proposal = proposal; + let psbt = proposal.apply_fee(None, None)?; + let txid = psbt.unsigned_tx.compute_txid(); + if let Some(psbt) = db_conn.spend_tx(&txid) { + let mut is_signed = false; + for psbtin in &psbt.inputs { + if !psbtin.partial_sigs.is_empty() { + log::debug!("[Payjoin] PSBT is signed!"); + is_signed = true; + break; } + } - if is_signed { - let proposal = proposal - .finalize_proposal( - |_| { - let mut psbt = psbt.clone(); - finalize_psbt(&mut psbt, secp); - Ok(psbt) - }, - None, - None, - ) - .save(persister)?; - - send_payjoin_proposal(proposal, persister)?; - } + if is_signed { + let proposal = proposal + .finalize_proposal( + |_| { + let mut psbt = psbt.clone(); + finalize_psbt(&mut psbt, secp); + Ok(psbt) + }, + None, + Some(FeeRate::from_sat_per_vb_unchecked(50)), + ) + .save(persister)?; + + send_payjoin_proposal(proposal, persister)?; } } Ok(()) From 2bcdefe233cc25986add6e0ac92371f3df037a69 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 11 Jul 2025 09:28:13 -0400 Subject: [PATCH 31/72] Resolve clippy linting warnings And cleaned up a unneeded TODO comment --- lianad/src/payjoin/helpers.rs | 6 +++--- lianad/src/payjoin/receiver.rs | 2 +- lianad/src/payjoin/sender.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 349661f63..efdfdf84a 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -26,7 +26,7 @@ pub enum FetchOhttpKeysError { impl std::error::Error for FetchOhttpKeysError {} impl std::fmt::Display for FetchOhttpKeysError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) + write!(f, "{self:?}") } } @@ -58,7 +58,7 @@ pub fn fetch_ohttp_keys( .header(ACCEPT, "application/ohttp-keys") .send() .map_err(|e| FetchOhttpKeysError::Reqwest(e.to_string()))?; - Ok(validate_ohttp_keys_response(res)?) + validate_ohttp_keys_response(res) } fn validate_ohttp_keys_response( @@ -89,7 +89,7 @@ pub fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1>) log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); db_conn.delete_spend(&original_txid); - // TODO(arturgontijo): Restoring witness_scripts and bip32_derivation so GUI can sign them + // Restoring witness_scripts and bip32_derivation so GUI can sign them for (index, psbtin) in proposal_psbt.inputs.iter_mut().enumerate() { let outpoint = &proposal_psbt.unsigned_tx.input[index].previous_output; if let Some(input) = input_fields_to_restore.get(outpoint) { From 2f2b64d3434a40fda905aab02c0ef0f128f4a17a Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 17 Jul 2025 08:10:16 -0400 Subject: [PATCH 32/72] hack: workaround getting payjoin status --- lianad/src/commands/mod.rs | 47 ++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 4dd31f8e0..1fb4dc127 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -27,6 +27,7 @@ use liana::{ }, }; +use log::info; use utils::{ deser_addr_assume_checked, deser_amount_from_sats, deser_fromstr, deser_hex, ser_amount, ser_hex, ser_to_string, @@ -457,7 +458,6 @@ impl DaemonControl { } /// Get Payjoin URI (BIP21) and its sender/receiver status by txid - /// TODO: this seems unused, can we remove it? pub fn get_payjoin_info( &self, txid: &bitcoin::Txid, @@ -470,35 +470,37 @@ impl DaemonControl { let (state, history) = replay_receiver_event_log(&persister).unwrap(); let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); if let Some(original_txid) = original_txid { - if original_txid == *txid { - let bip21 = history - .pj_uri() - .expect("should exist at this point") - .to_string(); - return Ok(Some(PayjoinInfo { - bip21, - status: state.into(), - })); - } + // if original_txid == *txid { + let bip21 = history + .pj_uri() + .expect("should exist at this point") + .to_string(); + return Ok(Some(PayjoinInfo { + bip21, + status: state.into(), + })); + // } } } for session_id in db_conn.get_all_sender_session_ids() { + log::info!("Checking sender session: {:?}", session_id); let persister = SenderPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); let (state, history) = replay_sender_event_log(&persister).unwrap(); + log::info!("Sender state: {:?}", state); let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); if let Some(original_txid) = original_txid { - if original_txid == *txid { - // TODO: this isnt a bip21, but a payjoin endpoint. Does this need to get returned? - let bip21 = history - .endpoint() - .expect("should exist at this point") - .to_string(); - return Ok(Some(PayjoinInfo { - bip21, - status: state.into(), - })); - } + // if original_txid == *txid { + // TODO: this isnt a bip21, but a payjoin endpoint. Does this need to get returned? + let bip21 = history + .endpoint() + .expect("should exist at this point") + .to_string(); + return Ok(Some(PayjoinInfo { + bip21, + status: state.into(), + })); + // } } } @@ -933,6 +935,7 @@ impl DaemonControl { // effort basis. let txid = tx.compute_txid(); if let Some(mut db_psbt) = db_conn.spend_tx(&txid) { + info!("Updating spend: {:?}", txid); let db_tx = db_psbt.unsigned_tx.clone(); for i in 0..db_tx.input.len() { if tx From 164b834ad168c4355c4bcf3fa69f29976eec1819 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 15 Jul 2025 15:41:06 -0400 Subject: [PATCH 33/72] Save psbt with fees applied --- Cargo.lock | 2 +- liana-gui/Cargo.toml | 3 +- lianad/Cargo.toml | 2 +- lianad/src/payjoin/receiver.rs | 56 +++++++++++++++++++++------------- lianad/src/payjoin/sender.rs | 4 +-- lianad/src/payjoin/types.rs | 3 +- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b505ae80f..6e1ea82e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,7 +4285,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.24.0" -source = "git+https://github.com/arminsabouri/rust-payjoin.git?branch=pub-apply-fee#4f7c7015f08778353641f661bfed9982626d3979" +source = "git+https://github.com/payjoin/rust-payjoin.git?branch=master#88b918f37a1af03ca3d48920ac8cd5ddcbe9dd44" dependencies = [ "bhttp", "bitcoin", diff --git a/liana-gui/Cargo.toml b/liana-gui/Cargo.toml index a59b06770..b54680c1e 100644 --- a/liana-gui/Cargo.toml +++ b/liana-gui/Cargo.toml @@ -55,7 +55,8 @@ rust-ini = "0.19.0" rfd = "0.15.1" # Payjoin -payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "pub-apply-fee", features = ["v2", "io"] } +payjoin = { git = "https://github.com/payjoin/rust-payjoin.git", branch = "master", features = ["v2", "io"] } + [target.'cfg(windows)'.dependencies] zip = { version = "0.6", default-features=false, features = ["bzip2", "deflate"] } diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index 3b0f9a75f..da0ba4806 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -60,5 +60,5 @@ jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = fal bip329 = { version = "0.3.0", default-features = false } # Payjoin -payjoin = { git = "https://github.com/arminsabouri/rust-payjoin.git", branch = "pub-apply-fee", features = ["v2", "io"] } +payjoin = { git = "https://github.com/payjoin/rust-payjoin.git", branch = "master", features = ["v2", "io"] } reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls", "stream", "blocking"] } diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 65037e802..1175f7e25 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -8,15 +8,14 @@ use liana::descriptors; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, FeeRate, OutPoint, Sequence, - TxIn, Weight, + consensus::encode::serialize_hex, psbt::Input, secp256k1, OutPoint, Sequence, TxIn, Weight, }, persist::OptionalTransitionOutcome, receive::{ v2::{ replay_event_log, Initialized, MaybeInputsOwned, MaybeInputsSeen, OutputsUnknown, PayjoinProposal, ProvisionalProposal, ReceiveSession, Receiver, UncheckedProposal, - WantsInputs, WantsOutputs, + WantsFeeRange, WantsInputs, WantsOutputs, }, InputPair, }, @@ -40,13 +39,13 @@ fn read_from_directory( ) -> Result<(), Box> { let mut receiver = receiver; let (req, context) = receiver - .extract_req(OHTTP_RELAY) + .create_poll_request(OHTTP_RELAY) .expect("Failed to extract request"); let proposal = match post_request(req.clone()) { Ok(ohttp_response) => { let response_bytes = ohttp_response.bytes()?; let state_transition = receiver - .process_res(response_bytes.as_ref(), context) + .process_response(response_bytes.as_ref(), context) .save(persister); match state_transition { Ok(OptionalTransitionOutcome::Progress(next_state)) => next_state, @@ -202,16 +201,31 @@ fn contribute_inputs( .clone(), ); - let mut proposal = proposal + let proposal = proposal .contribute_inputs(vec![selected_input])? .commit_inputs() .save(persister)?; - let psbt = proposal.apply_fee(None, None).unwrap(); + apply_fee_range(proposal, persister, db_conn, secp)?; + Ok(()) +} - db_conn.store_spend(psbt); +fn apply_fee_range( + proposal: Receiver, + persister: &ReceiverPersister, + db_conn: &mut Box, + secp: &secp256k1::Secp256k1, +) -> Result<(), Box> { + let proposal = proposal.apply_fee_range(None, None).save(persister)?; + let (_, session_history) = replay_event_log(persister)?; + let psbt = session_history + .psbt_ready_for_signing() + .expect("Just added fee applied psbt"); + + db_conn.store_spend(&psbt); log::info!("[Payjoin] PSBT in the DB..."); + finalize_proposal(proposal, persister, db_conn, secp)?; Ok(()) } @@ -221,8 +235,12 @@ fn finalize_proposal( db_conn: &mut Box, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { - let mut proposal = proposal; - let psbt = proposal.apply_fee(None, None)?; + let proposal = proposal; + let (_, session_history) = replay_event_log(persister)?; + let psbt = session_history + .psbt_ready_for_signing() + .expect("Just added fee applied psbt"); + let txid = psbt.unsigned_tx.compute_txid(); if let Some(psbt) = db_conn.spend_tx(&txid) { let mut is_signed = false; @@ -236,15 +254,11 @@ fn finalize_proposal( if is_signed { let proposal = proposal - .finalize_proposal( - |_| { - let mut psbt = psbt.clone(); - finalize_psbt(&mut psbt, secp); - Ok(psbt) - }, - None, - Some(FeeRate::from_sat_per_vb_unchecked(50)), - ) + .finalize_proposal(|_| { + let mut psbt = psbt.clone(); + finalize_psbt(&mut psbt, secp); + Ok(psbt) + }) .save(persister)?; send_payjoin_proposal(proposal, persister)?; @@ -258,7 +272,7 @@ fn send_payjoin_proposal( persister: &ReceiverPersister, ) -> Result<(), Box> { let (req, ctx) = proposal - .extract_req(OHTTP_RELAY) + .create_post_request(OHTTP_RELAY) .expect("Failed to extract request"); // Respond to sender @@ -266,7 +280,7 @@ fn send_payjoin_proposal( match post_request(req) { Ok(resp) => { proposal - .process_res(resp.bytes().expect("Failed to read response").as_ref(), ctx) + .process_response(resp.bytes().expect("Failed to read response").as_ref(), ctx) .save(persister)?; } Err(err) => log::error!("[Payjoin] send_payjoin_proposal(): {}", err), diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 22d2c8026..8b6f8ff2c 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -19,7 +19,7 @@ fn get_proposed_payjoin_psbt( persister: &SenderPersister, // TODO: replace with specific error ) -> Result, Box> { - let (req, ctx) = context.extract_req(OHTTP_RELAY)?; + let (req, ctx) = context.create_poll_request(OHTTP_RELAY)?; match post_request(req) { Ok(resp) => { let res = context @@ -48,7 +48,7 @@ fn post_orginal_proposal( sender: Sender, persister: &SenderPersister, ) -> Result<(), Box> { - let (req, ctx) = sender.extract_v2(OHTTP_RELAY)?; + let (req, ctx) = sender.create_v2_post_request(OHTTP_RELAY)?; match post_request(req) { Ok(resp) => { log::info!("[Payjoin] Posted original proposal..."); diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index 78c46a91f..c75663123 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -19,7 +19,8 @@ impl From for PayjoinStatus { | ReceiveSession::MaybeInputsSeen(_) | ReceiveSession::OutputsUnknown(_) | ReceiveSession::WantsOutputs(_) - | ReceiveSession::WantsInputs(_) => PayjoinStatus::Pending, + | ReceiveSession::WantsInputs(_) + | ReceiveSession::WantsFeeRange(_) => PayjoinStatus::Pending, ReceiveSession::ProvisionalProposal(_) => PayjoinStatus::WaitingToSign, ReceiveSession::PayjoinProposal(_) => PayjoinStatus::Success, ReceiveSession::TerminalFailure => PayjoinStatus::Failed, From 3eb48ee9e003d757d46ab61a45543cdddd0d2b7a Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 16 Jul 2025 10:54:42 -0400 Subject: [PATCH 34/72] Refactor schema to remove redundant session id --- lianad/src/database/mod.rs | 19 ++++------- lianad/src/database/sqlite/mod.rs | 51 +++++++++++----------------- lianad/src/database/sqlite/schema.rs | 6 ++-- lianad/src/payjoin/db.rs | 20 ++++++----- 4 files changed, 40 insertions(+), 56 deletions(-) diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 85baa94a6..0cf5f2ef7 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -203,11 +203,8 @@ pub trait DatabaseConnection { /// Save OHttpKeys fn payjoin_save_ohttp_keys(&mut self, ohttp_relay: &str, ohttp_keys: OhttpKeys); - /// Get the next Session Id - fn payjoin_next_id(&mut self, table: &str) -> u64; - /// Save Receiver Session - fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId); + fn save_new_payjoin_receiver_session(&mut self) -> i64; /// Get all Receiver Sessions fn get_all_receiver_session_ids(&mut self) -> Vec; @@ -223,7 +220,7 @@ pub trait DatabaseConnection { fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec>; /// Create a payjoin sender - fn save_new_payjoin_sender_session(&mut self, session_id: &SessionId); + fn save_new_payjoin_sender_session(&mut self) -> i64; /// Get a all active payjoin senders fn get_all_sender_session_ids(&mut self) -> Vec; @@ -466,12 +463,8 @@ impl DatabaseConnection for SqliteConn { self.payjoin_save_ohttp_keys(ohttp_relay, ohttp_keys) } - fn payjoin_next_id(&mut self, table: &str) -> u64 { - self.payjoin_next_id(table) - } - - fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId) { - self.save_new_payjoin_receiver_session(session_id) + fn save_new_payjoin_receiver_session(&mut self) -> i64 { + self.save_new_payjoin_receiver_session() } fn get_all_receiver_session_ids(&mut self) -> Vec { @@ -490,8 +483,8 @@ impl DatabaseConnection for SqliteConn { self.load_receiver_session_events(session_id) } - fn save_new_payjoin_sender_session(&mut self, session_id: &SessionId) { - self.save_new_payjoin_sender_session(session_id) + fn save_new_payjoin_sender_session(&mut self) -> i64 { + self.save_new_payjoin_sender_session() } fn get_all_sender_session_ids(&mut self) -> Vec { diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index d6872fe87..21f24c549 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -1016,45 +1016,31 @@ impl SqliteConn { .expect("Db must not fail"); } - /// Create a payjoin receiver - pub fn payjoin_next_id(&mut self, table: &str) -> u64 { - let count = db_query( - &mut self.conn, - &format!("SELECT COUNT(*) FROM {}", table), - rusqlite::params![], - |row| { - let count: u64 = row.get(0)?; - Ok(count) - }, - ) - .expect("Db must not fail"); - if let Some(count) = count.first() { - *count - } else { - 0 - } - } - /// Create new Receiver Session - pub fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId) { + pub fn save_new_payjoin_receiver_session(&mut self) -> i64 { + // TODO: is there a more elegant way to get the last insert row id atomically? + let mut id = 0i64; db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_receivers (session_id, created_at) VALUES (?1, ?2)", - rusqlite::params![session_id.0, curr_timestamp()], + "INSERT INTO payjoin_receivers (created_at) VALUES (?1)", + rusqlite::params![curr_timestamp()], )?; + + id = db_tx.last_insert_rowid(); Ok(()) }) .expect("Db must not fail"); + id } /// Get all active receiver session ids pub fn get_all_receiver_session_ids(&mut self) -> Vec { db_query( &mut self.conn, - "SELECT session_id FROM payjoin_receivers WHERE completed_at IS NULL", + "SELECT id FROM payjoin_receivers WHERE completed_at IS NULL", rusqlite::params![], |row| { - let id: u64 = row.get(0)?; + let id: i64 = row.get(0)?; Ok(SessionId::new(id)) }, ) @@ -1077,7 +1063,7 @@ impl SqliteConn { pub fn update_receiver_session_completed_at(&mut self, session_id: &SessionId) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_receivers SET completed_at = ?1 WHERE session_id = ?2", + "UPDATE payjoin_receivers SET completed_at = ?1 WHERE id = ?2", rusqlite::params![curr_timestamp(), session_id.0], )?; Ok(()) @@ -1099,24 +1085,27 @@ impl SqliteConn { .expect("Db must not fail") } - pub fn save_new_payjoin_sender_session(&mut self, session_id: &SessionId) { + pub fn save_new_payjoin_sender_session(&mut self) -> i64 { + let mut id = 0i64; db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_senders (session_id, created_at) VALUES (?1, ?2)", - rusqlite::params![session_id.0, curr_timestamp()], + "INSERT INTO payjoin_senders (created_at) VALUES (?1)", + rusqlite::params![curr_timestamp()], )?; + id = db_tx.last_insert_rowid(); Ok(()) }) .expect("Db must not fail"); + id } pub fn get_all_sender_session_ids(&mut self) -> Vec { db_query( &mut self.conn, - "SELECT session_id FROM payjoin_senders WHERE completed_at IS NULL", + "SELECT id FROM payjoin_senders WHERE completed_at IS NULL", rusqlite::params![], |row| { - let id: u64 = row.get(0)?; + let id: i64 = row.get(0)?; Ok(SessionId::new(id)) }, ) @@ -1137,7 +1126,7 @@ impl SqliteConn { pub fn update_sender_session_completed_at(&mut self, session_id: &SessionId) { db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "UPDATE payjoin_senders SET completed_at = ?1 WHERE session_id = ?2", + "UPDATE payjoin_senders SET completed_at = ?1 WHERE id = ?2", rusqlite::params![curr_timestamp(), session_id.0], )?; Ok(()) diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index edec937dd..9aa96a58b 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -134,7 +134,6 @@ CREATE TABLE payjoin_ohttp_keys ( /* Payjoin senders */ CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, - session_id INTEGER UNIQUE NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER ); @@ -145,13 +144,12 @@ CREATE TABLE payjoin_sender_events ( session_id INTEGER NOT NULL, created_at INTEGER NOT NULL, event BLOB NOT NULL, - FOREIGN KEY (session_id) REFERENCES payjoin_senders (session_id) + FOREIGN KEY (session_id) REFERENCES payjoin_senders (id) ); /* Payjoin receivers */ CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, - session_id INTEGER UNIQUE NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER ); @@ -162,7 +160,7 @@ CREATE TABLE payjoin_receiver_events ( session_id INTEGER NOT NULL, created_at INTEGER NOT NULL, event BLOB NOT NULL, - FOREIGN KEY (session_id) REFERENCES payjoin_receivers (session_id) + FOREIGN KEY (session_id) REFERENCES payjoin_receivers (id) ); "; diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index a4f04fe66..f1a2097c5 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -8,10 +8,10 @@ use std::sync::Arc; use crate::database::DatabaseInterface; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionId(pub u64); +pub struct SessionId(pub i64); impl SessionId { - pub fn new(id: u64) -> Self { + pub fn new(id: i64) -> Self { Self(id) } } @@ -44,9 +44,11 @@ pub struct ReceiverPersister { impl ReceiverPersister { pub fn new(db: Arc) -> Self { let mut db_conn = db.connection(); - let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_receivers")); - db_conn.save_new_payjoin_receiver_session(&session_id); - Self { db, session_id } + let session_id = db_conn.save_new_payjoin_receiver_session(); + Self { + db, + session_id: SessionId(session_id), + } } pub fn from_id(db: Arc, id: SessionId) -> Self { @@ -98,9 +100,11 @@ pub struct SenderPersister { impl SenderPersister { pub fn new(db: Arc) -> Self { let mut db_conn = db.connection(); - let session_id = SessionId::new(db_conn.payjoin_next_id("payjoin_senders")); - db_conn.save_new_payjoin_sender_session(&session_id); - Self { db, session_id } + let session_id = db_conn.save_new_payjoin_sender_session(); + Self { + db, + session_id: SessionId(session_id), + } } pub fn from_id(db: Arc, id: SessionId) -> Self { From 7186747bfd2202f813c88c628466ec59186a2f27 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 16 Jul 2025 11:06:06 -0400 Subject: [PATCH 35/72] Update payjoin persistance related test utils --- lianad/src/testutils.rs | 130 ++++++++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 25 deletions(-) diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index cd71cf10c..4c801ab98 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -9,10 +9,7 @@ use crate::{ DaemonControl, DaemonHandle, }; use liana::descriptors; -use payjoin::{ - receive::v2::SessionEvent as ReceiverSessionEvent, - send::v2::SessionEvent as SenderSessionEvent, OhttpKeys, -}; +use payjoin::OhttpKeys; use std::convert::TryInto; use std::{ @@ -154,6 +151,14 @@ impl BitcoinInterface for DummyBitcoind { } } +struct PayjoinSession { + completed: bool, +} + +struct PayjoinSessionEvent { + events: Vec>, +} + struct DummyDbState { deposit_index: bip32::ChildNumber, change_index: bip32::ChildNumber, @@ -165,6 +170,9 @@ struct DummyDbState { timestamp: u32, rescan_timestamp: Option, last_poll_timestamp: Option, + payjoin_sender_sessions: HashMap, + payjoin_receiver_sessions: HashMap, + payjoin_session_events: HashMap, } pub struct DummyDatabase { @@ -200,6 +208,9 @@ impl DummyDatabase { timestamp: now, rescan_timestamp: None, last_poll_timestamp: None, + payjoin_sender_sessions: HashMap::new(), + payjoin_receiver_sessions: HashMap::new(), + payjoin_session_events: HashMap::new(), })), } } @@ -559,56 +570,125 @@ impl DatabaseConnection for DummyDatabase { fn get_labels_bip329(&mut self, _offset: u32, _limit: u32) -> bip329::Labels { todo!() } - - fn get_all_receiver_session_ids(&mut self) -> Vec { - todo!() - } - fn save_new_payjoin_sender_session(&mut self, _session_id: &SessionId) { + fn payjoin_get_ohttp_keys(&mut self, _ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { todo!() } - fn payjoin_next_id(&mut self, _table: &str) -> u64 { + fn payjoin_save_ohttp_keys(&mut self, _ohttp_relay: &str, _ohttp_keys: payjoin::OhttpKeys) { todo!() } - fn get_all_sender_session_ids(&mut self) -> Vec { - todo!() + fn get_all_receiver_session_ids(&mut self) -> Vec { + self.db + .read() + .expect("lock should not be poisoned") + .payjoin_receiver_sessions + .keys() + .map(|id| SessionId(*id)) + .collect() } - - fn payjoin_get_ohttp_keys(&mut self, _ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { - todo!() + fn save_new_payjoin_sender_session(&mut self) -> i64 { + let id = self + .db + .read() + .expect("lock should not be poisoned") + .payjoin_sender_sessions + .len() as i64 + + 1; + self.db + .write() + .expect("lock should not be poisoned") + .payjoin_sender_sessions + .insert(id, PayjoinSession { completed: false }); + id } - fn payjoin_save_ohttp_keys(&mut self, _ohttp_relay: &str, _ohttp_keys: payjoin::OhttpKeys) { - todo!() + fn get_all_sender_session_ids(&mut self) -> Vec { + self.db + .read() + .expect("lock should not be poisoned") + .payjoin_sender_sessions + .keys() + .map(|id| SessionId(*id)) + .collect() } - fn save_new_payjoin_receiver_session(&mut self, session_id: &SessionId) { - todo!() + fn save_new_payjoin_receiver_session(&mut self) -> i64 { + let id = self + .db + .read() + .expect("lock should not be poisoned") + .payjoin_receiver_sessions + .len() as i64 + + 1; + self.db + .write() + .expect("lock should not be poisoned") + .payjoin_receiver_sessions + .insert(id, PayjoinSession { completed: false }); + id } fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec) { - todo!() + self.db + .write() + .expect("lock should not be poisoned") + .payjoin_session_events + .entry(session_id.0) + .or_insert(PayjoinSessionEvent { events: Vec::new() }) + .events + .push(event); } fn update_receiver_session_completed_at(&mut self, session_id: &SessionId) { - todo!() + self.db + .write() + .expect("lock should not be poisoned") + .payjoin_receiver_sessions + .entry(session_id.0) + .or_insert(PayjoinSession { completed: false }) + .completed = true; } fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec> { - todo!() + self.db + .read() + .expect("lock should not be poisoned") + .payjoin_session_events + .get(&session_id.0) + .map(|e| e.events.clone()) + .unwrap_or_default() } fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec) { - todo!() + self.db + .write() + .expect("lock should not be poisoned") + .payjoin_session_events + .entry(session_id.0) + .or_insert(PayjoinSessionEvent { events: Vec::new() }) + .events + .push(event); } fn get_all_sender_session_events(&mut self, session_id: &SessionId) -> Vec> { - todo!() + self.db + .read() + .expect("lock should not be poisoned") + .payjoin_session_events + .get(&session_id.0) + .map(|e| e.events.clone()) + .unwrap_or_default() } fn update_sender_session_completed_at(&mut self, session_id: &SessionId) { - todo!() + self.db + .write() + .expect("lock should not be poisoned") + .payjoin_receiver_sessions + .entry(session_id.0) + .or_insert(PayjoinSession { completed: false }) + .completed = true; } } From 69aa6f0295453b277e30945bae24b5776a238e09 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 17 Jul 2025 09:49:18 -0400 Subject: [PATCH 36/72] Add unit tests for payjoin db utils --- lianad/src/database/sqlite/mod.rs | 137 +++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 21f24c549..7df874021 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -3278,8 +3278,8 @@ CREATE TABLE labels ( // Two PSBTs we'll insert in the DB before and after the migration. Note they are random // PSBTs taken from the descriptor unit tests, it doesn't matter. - let first_psbt = psbt_from_str("cHNidP8BAIkCAAAAAWi3OFgkj1CqCDT3Swm8kbxZS9lxz4L3i4W2v9KGC7nqAQAAAAD9////AkANAwAAAAAAIgAg27lNc1rog+dOq80ohRuds4Hgg/RcpxVun2XwgpuLSrFYMwwAAAAAACIAIDyWveqaElWmFGkTbFojg1zXWHODtiipSNjfgi2DqBy9AAAAAAABAOoCAAAAAAEBsRWl70USoAFFozxc86pC7Dovttdg4kvja//3WMEJskEBAAAAAP7///8CWKmCIk4GAAAWABRKBWYWkCNS46jgF0r69Ehdnq+7T0BCDwAAAAAAIgAgTt5fs+CiB+FRzNC8lHcgWLH205sNjz1pT59ghXlG5tQCRzBEAiBXK9MF8z3bX/VnY2aefgBBmiAHPL4tyDbUOe7+KpYA4AIgL5kU0DFG8szKd+szRzz/OTUWJ0tZqij41h2eU9rSe1IBIQNBB1hy+jKsg1TihMT0dXw7etpu9TkO3NuvhBDFJlBj1cP2AQABAStAQg8AAAAAACIAIE7eX7PgogfhUczQvJR3IFix9tObDY89aU+fYIV5RubUIgICSKJsNs0zFJN58yd2aYQ+C3vhMbi0x7k0FV3wBhR4THlIMEUCIQCPWWWOhs2lThxOq/G8X2fYBRvM9MXSm7qPH+dRVYQZEwIgfut2vx3RvwZWcgEj4ohQJD5lNJlwOkA4PAiN1fjx6dABIgID3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACpHMEQCICZNR+0/1hPkrDQwPFmg5VjUHkh6aK9cXUu3kPbM8hirAiAyE/5NUXKfmFKij30isuyysJbq8HrURjivd+S9vdRGKQEBBZNSIQJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeSEC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8FSrnNkUSED3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACohA+ECH+HlR+8Sf3pumaXH3IwSsoqSLCH7H1THiBP93z3ZUq9SsmgiBgJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeRxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8Ec/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA95r49c3q2SqITlYSgorJGJPt6qwrpujyMA4UNenjwAqHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAEAAAAiBgPhAh/h5UfvEn96bpmlx9yMErKKkiwh+x9Ux4gT/d892Rz/1jyNMAAAgAEAAIABAACAAgAAgAAAAAABAAAAACICAlBQ7gGocg7eF3sXrCio+zusAC9+xfoyIV95AeR69DWvHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAMAAAAiAgMvVy984eg8Kgvj058PBHetFayWbRGb7L0DMnS9KHSJzBxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgIDSRIG1dn6njdjsDXenHa2lUvQHWGPLKBVrSzbQOhiIxgc/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAwAAACICA0/epE59sVEj7Et0I4R9qJQNuX23RNvDZKCRL7eUps9FHP/WPI0wAACAAQAAgAEAAIACAACAAAAAAAMAAAAAIgICgldCOK6iHscv//2NipgaMABLV5TICU/zlP7HlQmlg08cY2rfPzAAAIABAACAAQAAgAIAAIABAAAAAQAAACICApb0p9rfpJshB3J186PGWrvzQdixcwQZWmebOUMdkquZHP/WPI0wAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgLY5q+unoDxC/HI5BaNiPq12ei1REZIcUAN304JfKXUwxz/1jyNMAAAgAEAAIABAACAAgAAgAEAAAABAAAAIgIDg6cUVCJB79cMcofiURHojxFARWyS4YEhJNRixuOZZRgcY2rfPzAAAIABAACAAAAAgAIAAIABAAAAAQAAAAA="); - let second_psbt = psbt_from_str("cHNidP8BAP0fAQIAAAAGAGo6V8K5MtKcQ8vRFedf5oJiOREiH4JJcEniyRv2800BAAAAAP3///9e3dVLjWKPAGwDeuUOmKFzOYEP5Ipu4LWdOPA+lITrRgAAAAAA/f///7cl9oeu9ssBXKnkWMCUnlgZPXhb+qQO2+OPeLEsbdGkAQAAAAD9////idkxRErbs34vsHUZ7QCYaiVaAFDV9gxNvvtwQLozwHsAAAAAAP3///9EakyJhd2PjwYh1I7zT2cmcTFI5g1nBd3srLeL7wKEewIAAAAA/f///7BcaP77nMaA2NjT/hyI6zueB/2jU/jK4oxmSqMaFkAzAQAAAAD9////AUAfAAAAAAAAFgAUqo7zdMr638p2kC3bXPYcYLv9nYUAAAAAAAEA/X4BAgAAAAABApEoe5xCmSi8hNTtIFwsy46aj3hlcLrtFrug39v5wy+EAQAAAGpHMEQCIDeI8JTWCTyX6opCCJBhWc4FytH8g6fxDaH+Wa/QqUoMAiAgbITpz8TBhwxhv/W4xEXzehZpOjOTjKnPw36GIy6SHAEhA6QnYCHUbU045FVh6ZwRwYTVineqRrB9tbqagxjaaBKh/v///+v1seDE9gGsZiWwewQs3TKuh0KSBIHiEtG8ABbz2DpAAQAAAAD+////Aqhaex4AAAAAFgAUkcVOEjVMct0jyCzhZN6zBT+lvTQvIAAAAAAAACIAIKKDUd/GWjAnwU99llS9TAK2dK80/nSRNLjmrhj0odUEAAJHMEQCICSn+boh4ItAa3/b4gRUpdfblKdcWtMLKZrgSEFFrC+zAiBtXCx/Dq0NutLSu1qmzFF1lpwSCB3w3MAxp5W90z7b/QEhA51S2ERUi0bg+l+bnJMJeAfDknaetMTagfQR9+AOrVKlxdMkAAEBKy8gAAAAAAAAIgAgooNR38ZaMCfBT32WVL1MArZ0rzT+dJE0uOauGPSh1QQiAgN+zbSfdr8oJBtlKomnQTHynF2b/UhovAwf0eS8awRSqUgwRQIhAJhm6xQvxt2LY+eNZqjhsgMOAxD0OPYty6nf9WaQZtgkAiBf/AXkeyq6ALknO9TZwY6ZRa0evY+DQ3j3XaqiBiAMfgEBBUEhA37NtJ92vygkG2UqiadBMfKcXZv9SGi8DB/R5LxrBFKprHNkdqkUxttmGj2sqzzaxSaacJTnJPDCbY6IrVqyaCIGAv9qeBDEB+5kvM/sZ8jQ7QApfZcDrqtq5OAe2gQ1V+pmDIpk8qkAAAAA0AAAACIGA37NtJ92vygkG2UqiadBMfKcXZv9SGi8DB/R5LxrBFKpDPWswv0AAAAA0AAAAAABAOoCAAAAAAEB0OPoVJs9ihvnAwjO16k/wGJuEus1IEE1Yo2KBjC2NSEAAAAAAP7///8C6AMAAAAAAAAiACBfeUS9jQv6O1a96Aw/mPV6gHxHl3mfj+f0frfAs2sMpP1QGgAAAAAAFgAUDS4UAIpdm1RlFYmg0OoCxW0yBT4CRzBEAiAPvbNlnhiUxLNshxN83AuK/lGWwlpXOvmcqoxsMLzIKwIgWwATJuYPf9buLe9z5SnXVnPVL0q6UZaWE5mjCvEl1RUBIQI54LFZmq9Lw0pxKpEGeqI74NnIfQmLMDcv5ySplUS1/wDMJAABASvoAwAAAAAAACIAIF95RL2NC/o7Vr3oDD+Y9XqAfEeXeZ+P5/R+t8CzawykIgICYn4eZbb6KGoxB1PEv/XPiujZFDhfoi/rJPtfHPVML2lHMEQCIDOHEqKdBozXIPLVgtBj3eWC1MeIxcKYDADe4zw0DbcMAiAq4+dbkTNCAjyCxJi0TKz5DWrPulxrqOdjMRHWngXHsQEBBUEhAmJ+HmW2+ihqMQdTxL/1z4ro2RQ4X6Iv6yT7Xxz1TC9prHNkdqkUzc/gCLoe6rQw63CGXhIR3YRz1qCIrVqyaCIGAmJ+HmW2+ihqMQdTxL/1z4ro2RQ4X6Iv6yT7Xxz1TC9pDPWswv0AAAAAqgAAACIGA8JCTIzdSoTJhiKN1pn+NnlkyuKOndiTgH2NIX+yNsYqDIpk8qkAAAAAqgAAAAABAOoCAAAAAAEBRGpMiYXdj48GIdSO809nJnExSOYNZwXd7Ky3i+8ChHsAAAAAAP7///8COMMQAAAAAAAWABQ5rnyuG5T8iuhqfaGAmpzlybo3t+gDAAAAAAAAIgAg7Kz3CX1RBjIvbK9LBYztmi7F1XIxQpX6mtCUkflvvl8CRzBEAiBaYx4sOHckEZwDnSrbb1ivc6seX4Puasm1PBGnBWgSTQIgCeUiXvd90ajI3F4/BHifLUI4fVIgVQFCqLTbbeXQD5oBIQOmGm+gTRx1slzF+wn8NhZoR1xfSYgoKX6bpRSVRjLcEXrOJAABASvoAwAAAAAAACIAIOys9wl9UQYyL2yvSwWM7ZouxdVyMUKV+prQlJH5b75fIgID0X2UJhC5+2jgJqUrihxZxDZHK7jgPFlrUYzoSHQTmP9HMEQCIEM4K8lVACvE2oSMZHDJiOeD81qsYgAvgpRgcSYgKc3AAiAQjdDr2COBea69W+2iVbnODuH3QwacgShW3dS4yeggJAEBBUEhA9F9lCYQufto4CalK4ocWcQ2Ryu44DxZa1GM6Eh0E5j/rHNkdqkU0DTexcgOQQ+BFjgS031OTxcWiH2IrVqyaCIGA9F9lCYQufto4CalK4ocWcQ2Ryu44DxZa1GM6Eh0E5j/DPWswv0AAAAAvwAAACIGA/xg4Uvem3JHVPpyTLP5JWiUH/yk3Y/uUI6JkZasCmHhDIpk8qkAAAAAvwAAAAABAOoCAAAAAAEBmG+mPq0O6QSWEMctsMjvv5LzWHGoT8wsA9Oa05kxIxsBAAAAAP7///8C6AMAAAAAAAAiACDUvIILFr0OxybADV3fB7ms7+ufnFZgicHR0nbI+LFCw1UoGwAAAAAAFgAUC+1ZjCC1lmMcvJ/4JkevqoZF4igCRzBEAiA3d8o96CNgNWHUkaINWHTvAUinjUINvXq0KBeWcsSWuwIgKfzRNWFR2LDbnB/fMBsBY/ylVXcSYwLs8YC+kmko1zIBIQOpEfsLv0htuertA1sgzCwGvHB0vE4zFO69wWEoHClKmAfMJAABASvoAwAAAAAAACIAINS8ggsWvQ7HJsANXd8Huazv65+cVmCJwdHSdsj4sULDIgID96jZc0sCi0IIXf2CpfE7tY+9LRmMsOdSTTHelFxfCwJHMEQCIHlaiMMznx8Cag8Y3X2gXi9Qtg0ZuyHEC6DsOzipSGOKAiAV2eC+S3Mbq6ig5QtRvTBsq5M3hCBdEJQlOrLVhWWt6AEBBUEhA/eo2XNLAotCCF39gqXxO7WPvS0ZjLDnUk0x3pRcXwsCrHNkdqkUyJ+Cbx7vYVY665yjJnMNODyYrAuIrVqyaCIGAt8UyDXk+mW3Y6IZNIBuDJHkdOaZi/UEShkN5L3GiHR5DIpk8qkAAAAAuAAAACIGA/eo2XNLAotCCF39gqXxO7WPvS0ZjLDnUk0x3pRcXwsCDPWswv0AAAAAuAAAAAABAP0JAQIAAAAAAQG7Zoy4I3J9x+OybAlIhxVKcYRuPFrkDFJfxMiC3kIqIAEAAAAA/v///wO5xxAAAAAAABYAFHgBzs9wJNVk6YwR81IMKmckTmC56AMAAAAAAAAWABTQ/LmJix5JoHBOr8LcgEChXHdLROgDAAAAAAAAIgAg7Kz3CX1RBjIvbK9LBYztmi7F1XIxQpX6mtCUkflvvl8CRzBEAiA+sIKnWVE3SmngjUgJdu1K2teW6eqeolfGe0d11b+irAIgL20zSabXaFRNM8dqVlcFsfNJ0exukzvxEOKl/OcF8VsBIQJrUspHq45AMSwbm24//2a9JM8XHFWbOKpyV+gNCtW71nrOJAABASvoAwAAAAAAACIAIOys9wl9UQYyL2yvSwWM7ZouxdVyMUKV+prQlJH5b75fIgID0X2UJhC5+2jgJqUrihxZxDZHK7jgPFlrUYzoSHQTmP9IMEUCIQCmDhJ9fyhlQwPruoOUemDuldtRu3ZkiTM3DA0OhkguSQIgYerNaYdP43DcqI5tnnL3n4jEeMHFCs+TBkOd6hDnqAkBAQVBIQPRfZQmELn7aOAmpSuKHFnENkcruOA8WWtRjOhIdBOY/6xzZHapFNA03sXIDkEPgRY4EtN9Tk8XFoh9iK1asmgiBgPRfZQmELn7aOAmpSuKHFnENkcruOA8WWtRjOhIdBOY/wz1rML9AAAAAL8AAAAiBgP8YOFL3ptyR1T6ckyz+SVolB/8pN2P7lCOiZGWrAph4QyKZPKpAAAAAL8AAAAAAQDqAgAAAAABAT6/vc6qBRzhQyjVtkC25NS2BvGyl2XjjEsw3e8vAesjAAAAAAD+////AgPBAO4HAAAAFgAUEwiWd/qI1ergMUw0F1+qLys5G/foAwAAAAAAACIAIOOPEiwmp2ZXR7ciyrveITXw0tn6zbQUA1Eikd9QlHRhAkcwRAIgJMZdO5A5u2UIMrAOgrR4NcxfNgZI6OfY7GKlZP0O8yUCIDFujbBRnamLEbf0887qidnXo6UgQA9IwTx6Zomd4RvJASEDoNmR2/XcqSyCWrE1tjGJ1oLWlKt4zsFekK9oyB4Hl0HF0yQAAQEr6AMAAAAAAAAiACDjjxIsJqdmV0e3Isq73iE18NLZ+s20FANRIpHfUJR0YSICAo3uyJxKHR9Z8fwvU7cywQCnZyPvtMl3nv54wPW1GSGqSDBFAiEAlLY98zqEL/xTUvm9ZKy5kBa4UWfr4Ryu6BmSZjseXPQCIGy7efKbZLQSDq8RhgNNjl1384gWFTN7nPwWV//SGriyAQEFQSECje7InEodH1nx/C9TtzLBAKdnI++0yXee/njA9bUZIaqsc2R2qRQhPRlaLsh/M/K/9fvbjxF/M20cNoitWrJoIgYCF7Rj5jFhe5L6VDzP5m2BeaG0mA9e7+6fMeWkWxLwpbAMimTyqQAAAADNAAAAIgYCje7InEodH1nx/C9TtzLBAKdnI++0yXee/njA9bUZIaoM9azC/QAAAADNAAAAAAA="); + let first_psbt = psbt_from_str("cHNidP8BAIkCAAAAAWi3OFgkj1CqCDT3Swm8kbxZS9lxz4L3i4W2v9KGC7nqAQAAAAD9////AkANAwAAAAAAIgAg27lNc1rog+dOq80ohRuds4Hgg/RcpxVun2XwgpuLSrFYMwwAAAAAACIAIDyWveqaElWmFGkTbFojg1zXWHODtiipSNjfgi2DqBy9AAAAAAABAOoCAAAAAAEBsRWl70USoAFFozxc86pC7Dovttdg4kvja//3WMEJskEBAAAAAP7///8CWKmCIk4GAAAWABRKBWYWkCNS46jgF0r69Ehdnq+7T0BCDwAAAAAAIgAgTt5fs+CiB+FRzNC8lHcgWLH205sNjz1pT59ghXlG5tQCRzBEAiBXK9MF8z3bX/VnY2aefgBBmiAHPL4tyDbUOe7+KpYA4AIgL5kU0DFG8szKd+szRzz/OTUWJ0tZqij41h2eU9rSe1IBIQNBB1hy+jKsg1TihMT0dXw7etpu9TkO3NuvhBDFJlBj1cP2AQABAStAQg8AAAAAACIAIE7eX7PgogfhUczQvJR3IFix9tObDY89aU+fYIV5RubUIgICSKJsNs0zFJN58yd2aYQ+C3vhMbi0x7k0FV3wBhR4THlIMEUCIQCPWWWOhs2lThxOq/G8X2fYBRvM9MXSm7qPH+dRVYQZEwIgfut2vx3RvwZWcgEj4ohQJD5lNJlwOkA4PAiN1fjx6dABIgID3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACpHMEQCICZNR+0/1hPkrDQwPFmg5VjUHkh6aK9cXUu3kPbM8hirAiAyE/5NUXKfmFKij30isuyysJbq8HrURjivd+S9vdRGKQEBBZNSIQJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeSEC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8FSrnNkUSED3mvj1zerZKohOVhKCiskYk+3qrCum6PIwDhQ16ePACohA+ECH+HlR+8Sf3pumaXH3IwSsoqSLCH7H1THiBP93z3ZUq9SsmgiBgJIomw2zTMUk3nzJ3ZphD4Le+ExuLTHuTQVXfAGFHhMeRxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgYC9OfCXl+sJOrxUFLBuMV4ZUlJYjuzNGZSld5ioY14y8Ec/9Y8jTAAAIABAACAAAAAgAIAAIAAAAAAAQAAACIGA95r49c3q2SqITlYSgorJGJPt6qwrpujyMA4UNenjwAqHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAEAAAAiBgPhAh/h5UfvEn96bpmlx9yMErKKkiwh+x9Ux4gT/d892Rz/1jyNMAAAgAEAAIABAACAAgAAgAAAAAABAAAAACICAlBQ7gGocg7eF3sXrCio+zusAC9+xfoyIV95AeR69DWvHGNq3z8wAACAAQAAgAEAAIACAACAAAAAAAMAAAAiAgMvVy984eg8Kgvj058PBHetFayWbRGb7L0DMnS9KHSJzBxjat8/MAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgIDSRIG1dn6njdjsDXenHa2lUvQHWGPLKBVrSzbQOhiIxgc/9Y8jTAAAIABAACAAAAAgAIAAIABAAAAAQAAACICApb0p9rfpJshB3J186PGWrvzQdixcwQZWmebOUMdkquZHP/WPI0wAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgLY5q+unoDxC/HI5BaNiPq12ei1REZIcUAN304JfKXUwxz/1jyNMAAAgAEAAIABAACAAgAAgAEAAAABAAAAIgIDg6cUVCJB79cMcofiURHojxFARWyS4YEhJNRixuOZZRgcY2rfPzAAAIABAACAAAAAgAIAAIABAAAAAQAAAAA="); + let second_psbt = psbt_from_str("cHNidP8BAP0fAQIAAAAGAGo6V8K5MtKcQ8vRFedf5oJiOREiH4JJcEniyRv2800BAAAAAP3///9e3dVLjWKPAGwDeuUOmKFzOYEP5Ipu4LWdOPA+lITrRgAAAAAA/f///7cl9oeu9ssBXKnkWMCUnlgZPXhb+qQO2+OPeLEsbdGkAQAAAAD9////idkxRErbs34vsHUZ7QCYaiVaAFDV9gxNvvtwQLozwHsAAAAAAP3///9EakyJhd2PjwYh1I7zT2cmcTFI5g1nBd3srLeL7wKEewIAAAAA/f///7BcaP77nMaA2NjT/hyI6zueB/2jU/jK4oxmSqMaFkAzAQAAAAD9////AUAfAAAAAAAAFgAUqo7zdMr638p2kC3bXPYcYLv9nYUAAAAAAAEA/X4BAgAAAAABApEoe5xCmSi8hNTtIFwsy46aj3hlcLrtFrug39v5wy+EAQAAAGpHMEQCIDeI8JTWCTyX6opCCJBhWc4FytH8g6fxDaH+Wa/QqUoMAiAgbITpz8TBhwxhv/W4xEXzehZpOjOTjKnPw36GIy6SHAEhA6QnYCHUbU045FVh6ZwRwYTVineqRrB9tbqagxjaaBKh/v///+v1seDE9gGsZiWwewQs3TKuh0KSBIHiEtG8ABbz2DpAAQAAAAD+////Aqhaex4AAAAAFgAUkcVOEjVMct0jyCzhZN6zBT+lvTQvIAAAAAAAACIAIKKDUd/GWjAnwU99llS9TAK2dK80/nSRNLjmrhj0odUEAAJHMEQCICSn+boh4ItAa3/b4gRUpdfblKdcWtMLKZrgSEFFrC+zAiBtXCx/Dq0NutLSu1qmzFF1lpwSCB3w3MAxp5W90z7b/QEhA51S2ERUi0bg+l+bnJMJeAfDknaetMTagfQR9+AOrVKlxdMkAAEBKy8gAAAAAAAAIgAgooNR38ZaMCfBT32WVL1MArZ0rzT+dJE0uOauGPSh1QQiAgN+zbSfdr8oJBtlKomnQTHynF2b/UhovAwf0eS8awRSqUgwRQIhAJhm6xQvxt2LY+eNZqjhsgMOAxD0OPYty6nf9WaQZtgkAiBf/AXkeyq6ALknO9TZwY6ZRa0evY+DQ3j3XaqiBiAMfgEBBUEhA37NtJ92vygkG2UqiadBMfKcXZv9SGi8DB/R5LxrBFKprHNkdqkUxttmGj2sqzzaxSaacJTnJPDCbY6IrVqyaCIGAv9qeBDEB+5kvM/sZ8jQ7QApfZcDrqtq5OAe2gQ1V+pmDIpk8qkAAAAA0AAAACIGA37NtJ92vygkG2UqiadBMfKcXZv9SGi8DB/R5LxrBFKpDPWswv0AAAAA0AAAAAABAOoCAAAAAAEB0OPoVJs9ihvnAwjO16k/wGJuEus1IEE1Yo2KBjC2NSEAAAAAAP7///8C6AMAAAAAAAAiACBfeUS9jQv6O1a96Aw/mPV6gHxHl3mfj+f0frfAs2sMpP1QGgAAAAAAFgAUDS4UAIpdm1RlFYmg0OoCxW0yBT4CRzBEAiAPvbNlnhiUxLNshxN83AuK/lGWwlpXOvmcqoxsMLzIKwIgWwATJuYPf9buLe9z5SnXVnPVL0q6UZaWE5mjCvEl1RUBIQI54LFZmq9Lw0pxKpEGeqI74NnIfQmLMDcv5ySplUS1/wDMJAABASvoAwAAAAAAACIAIF95RL2NC/o7Vr3oDD+Y9XqAfEeXeZ+P5/R+t8CzawykIgICYn4eZbb6KGoxB1PEv/XPiujZFDhfoi/rJPtfHPVML2lHMEQCIDOHEqKdBozXIPLVgtBj3eWC1MeIxcKYDADe4zw0DbcMAiAq4+dbkTNCAjyCxJi0TKz5DWrPulxrqOdjMRHWngXHsQEBBUEhAmJ+HmW2+ihqMQdTxL/1z4ro2RQ4X6Iv6yT7Xxz1TC9prHNkdqkUzc/gCLoe6rQw63CGXhIR3YRz1qCIrVqyaCIGAmJ+HmW2+ihqMQdTxL/1z4ro2RQ4X6Iv6yT7Xxz1TC9pDPWswv0AAAAAuAAAACIGA/xg4Uvem3JHVPpyTLP5JWiUH/yk3Y/uUI6JkZasCmHhDIpk8qkAAAAAvwAAAAABAOoCAAAAAAEBmG+mPq0O6QSWEMctsMjvv5LzWHGoT8wsA9Oa05kxIxsBAAAAAP7///8C6AMAAAAAAAAiACDUvIILFr0OxybADV3fB7ms7+ufnFZgicHR0nbI+LFCw1UoGwAAAAAAFgAUC+1ZjCC1lmMcvJ/4JkevqoZF4igCRzBEAiA3d8o96CNgNWHUkaINWHTvAUinjUINvXq0KBeWcsSWuwIgKfzRNWFR2LDbnB/fMBsBY/ylVXcSYwLs8YC+kmko1zIBIQOpEfsLv0htuertA1sgzCwGvHB0vE4zFO69wWEoHClKmAfMJAABASvoAwAAAAAAACIAINS8ggsWvQ7HJsANXd8Huazv65+cVmCJwdHSdsj4sULDIgID96jZc0sCi0IIXf2CpfE7tY+9LRmMsOdSTTHelFxfCwJHMEQCIHlaiMMznx8Cag8Y3X2gXi9Qtg0ZuyHEC6DsOzipSGOKAiAV2eC+S3Mbq6ig5QtRvTBsq5M3hCBdEJQlOrLVhWWt6AEBBUEhA/eo2XNLAotCCF39gqXxO7WPvS0ZjLDnUk0x3pRcXwsCrHNkdqkUyJ+Cbx7vYVY665yjJnMNODyYrAuIrVqyaCIGAt8UyDXk+mW3Y6IZNIBuDJHkdOaZi/UEShkN5L3GiHR5DIpk8qkAAAAAuAAAACIGA/eo2XNLAotCCF39gqXxO7WPvS0ZjLDnUk0x3pRcXwsCDPWswv0AAAAAuAAAAAABAP0JAQIAAAAAAQG7Zoy4I3J9x+OybAlIhxVKcYRuPFrkDFJfxMiC3kIqIAEAAAAA/v///wO5xxAAAAAAABYAFHgBzs9wJNVk6YwR81IMKmckTmC56AMAAAAAAAAWABTQ/LmJix5JoHBOr8LcgEChXHdLROgDAAAAAAAAIgAg7Kz3CX1RBjIvbK9LBYztmi7F1XIxQpX6mtCUkflvvl8CRzBEAiA+sIKnWVE3SmngjUgJdu1K2teW6eqeolfGe0d11b+irAIgL20zSabXaFRNM8dqVlcFsfNJ0exukzvxEOKl/OcF8VsBIQJrUspHq45AMSwbm24//2a9JM8XHFWbOKpyV+gNCtW71nrOJAABASvoAwAAAAAAACIAIOys9wl9UQYyL2yvSwWM7ZouxdVyMUKV+prQlJH5b75fIgID0X2UJhC5+2jgJqUrihxZxDZHK7jgPFlrUYzoSHQTmP9HMEQCIEM4K8lVACvE2oSMZHDJiOeD81qsYgAvgpRgcSYgKc3AAiAQjdDr2COBea69W+2iVbnODuH3QwacgShW3dS4yeggJAEBBUEhA9F9lCYQufto4CalK4ocWcQ2Ryu44DxZa1GM6Eh0E5j/rHNkdqkU0DTexcgOQQ+BFjgS031OTxcWiH2IrVqyaCIGA9F9lCYQufto4CalK4ocWcQ2Ryu44DxZa1GM6Eh0E5j/DPWswv0AAAAAvwAAACIGA/xg4Uvem3JHVPpyTLP5JWiUH/yk3Y/uUI6JkZasCmHhDIpk8qkAAAAAvwAAAAABAOoCAAAAAAEBmG+mPq0O6QSWEMctsMjvv5LzWHGoT8wsA9Oa05kxIxsBAAAAAP7///8C6AMAAAAAAAAiACDUvIILFr0OxybADV3fB7ms7+ufnFZgicHR0nbI+LFCw1UoGwAAAAAAFgAUC+1ZjCC1lmMcvJ/4JkevqoZF4igCRzBEAiA3d8o96CNgNWHUkaINWHTvAUinjUINvXq0KBeWcsSWuwIgKfzRNWFR2LDbnB/fMBsBY/ylVXcSYwLs8YC+kmko1zIBIQOpEfsLv0htuertA1sgzCwGvHB0vE4zFO69wWEoHClKmAfMJAABASvoAwAAAAAAACIAINS8ggsWvQ7HJsANXd8Huazv65+cVmCJwdHSdsj4sULDIgID96jZc0sCi0IIXf2CpfE7tY+9LRmMsOdSTTHelFxfCwJHMEQCIHlaiMMznx8Cag8Y3X2gXi9Qtg0ZuyHEC6DsOzipSGOKAiAV2eC+S3Mbq6ig5QtRvTBsq5M3hCBdEJQlOrLVhWWt6AEBBUEhA/eo2XNLAotCCF39gqXxO7WPvS0ZjLDnUk0x3pRcXwsCrHNkdqkUyJ+Cbx7vYVY665yjJnMNODyYrAuIrVqyaCIGAt8UyDXk+mW3Y6IZNIBuDJHkdOaZi/UEShkN5L3GiHR5DIpk8qkAAAAAuAAAACIGA/eo2XNLAotCCF39gqXxO7WPvS0ZjLDnUk0x3pRcXwsCDPWswv0AAAAAuAAAAAABAP0JAQIAAAAAAQG7Zoy4I3J9x+OybAlIhxVKcYRuPFrkDFJfxMiC3kIqIAEAAAAA/v///wO5xxAAAAAAABYAFHgBzs9wJNVk6YwR81IMKmckTmC56AMAAAAAAAAWABTQ/LmJix5JoHBOr8LcgEChXHdLROgDAAAAAAAAIgAg7Kz3CX1RBjIvbK9LBYztmi7F1XIxQpX6mtCUkflvvl8CRzBEAiA+sIKnWVE3SmngjUgJdu1K2teW6eqeolfGe0d11b+irAIgL20zSabXaFRNM8dqVlcFsfNJ0exukzvxEOKl/OcF8VsBIQJrUspHq45AMSwbm24//2a9JM8XHFWbOKpyV+gNCtW71nrOJAABASvoAwAAAAAAACIAIOys9wl9UQYyL2yvSwWM7ZouxdVyMUKV+prQlJH5b75fIgID0X2UJhC5+2jgJqUrihxZxDZHK7jgPFlrUYzoSHQTmP9IMEUCIQCmDhJ9fyhlQwPruoOUemDuldtRu3ZkiTM3DA0OhkguSQIgYerNaYdP43DcqI5tnnL3n4jEeMHFCs+TBkOd6hDnqAkBAQVBIQPRfZQmELn7aOAmpSuKHFnENkcruOA8WWtRjOhIdBOY/6xzZHapFNA03sXIDkEPgRY4EtN9Tk8XFoh9iK1asmgiBgPRfZQmELn7aOAmpSuKHFnENkcruOA8WWtRjOhIdBOY/wz1rML9AAAAAL8AAAAiBgP8YOFL3ptyR1T6ckyz+SVolB/8pN2P7lCOiZGWrAph4QyKZPKpAAAAAL8AAAAAAQDqAgAAAAABAT6/vc6qBRzhQyjVtkC25NS2BvGyl2XjjEsw3e8vAesjAAAAAAD+////AgPBAO4HAAAAFgAUEwiWd/qI1ergMUw0F1+qLys5G/foAwAAAAAAACIAIOOPEiwmp2ZXR7ciyrveITXw0tn6zbQUA1Eikd9QlHRhAkcwRAIgJMZdO5A5u2UIMrAOgrR4NcxfNgZI6OfY7GKlZP0O8yUCIDFujbBRnamLEbf0887qidnXo6UgQA9IwTx6Zomd4RvJASEDoNmR2/XcqSyCWrE1tjGJ1oLWlKt4zsFekK9oyB4Hl0HF0yQAAQEr6AMAAAAAAAAiACDjjxIsJqdmV0e3Isq73iE18NLZ+s20FANRIpHfUJR0YSICAo3uyJxKHR9Z8fwvU7cywQCnZyPvtMl3nv54wPW1GSGqSDBFAiEAlLY98zqEL/xTUvm9ZKy5kBa4UWfr4Ryu6BmSZjseXPQCIGy7efKbZLQSDq8RhgNNjl1384gWFTN7nPwWV//SGriyAQEFQSECje7InEodH1nx/C9TtzLBAKdnI++0yXee/njA9bUZIaqsc2R2qRQhPRlaLsh/M/K/9fvbjxF/M20cNoitWrJoIgYCF7Rj5jFhe5L6VDzP5m2BeaG0mA9e7+6fMeWkWxLwpbAMimTyqQAAAADNAAAAIgYCje7InEodH1nx/C9TtzLBAKdnI++0yXee/njA9bUZIaoM9azC/QAAAADNAAAAAAA="); let bitcoin_txs: Vec<_> = (0..2) .map(|i| bitcoin::Transaction { @@ -3753,4 +3753,137 @@ CREATE TABLE labels ( fs::remove_dir_all(tmp_dir).unwrap(); } + + #[test] + fn test_payjoin_receiver_sessions() { + let (temp_dir, _, _, db) = dummy_db(); + let mut conn = db.connection().unwrap(); + + let session_id_1 = conn.save_new_payjoin_receiver_session(); + assert!(session_id_1 > 0); + let session_id_2 = conn.save_new_payjoin_receiver_session(); + assert!(session_id_2 > session_id_1); + + let active_sessions = conn.get_all_receiver_session_ids(); + assert_eq!(active_sessions.len(), 2); + assert!(active_sessions.iter().any(|s| s.0 == session_id_1)); + assert!(active_sessions.iter().any(|s| s.0 == session_id_2)); + + let event_data = b"test event data".to_vec(); + let session_id = SessionId::new(session_id_1); + conn.save_receiver_session_event(&session_id, event_data.clone()); + + let events = conn.load_receiver_session_events(&session_id); + assert_eq!(events.len(), 1); + assert_eq!(events[0], event_data); + + conn.update_receiver_session_completed_at(&session_id); + // This should not change how events are loaded + let events = conn.load_receiver_session_events(&session_id); + assert_eq!(events.len(), 1); + assert_eq!(events[0], event_data); + + // Verify session is no longer active + let active_sessions_after = conn.get_all_receiver_session_ids(); + assert_eq!(active_sessions_after.len(), 1); + assert!(!active_sessions_after.iter().any(|s| s.0 == session_id_1)); + + let event_data_2 = b"second event data".to_vec(); + // Data is same as the second event + let event_data_3 = b"second event data".to_vec(); + conn.save_receiver_session_event(&session_id, event_data_2.clone()); + conn.save_receiver_session_event(&session_id, event_data_3.clone()); + + let all_events = conn.load_receiver_session_events(&session_id); + assert_eq!(all_events.len(), 3); + assert_eq!(all_events[0], event_data); + assert_eq!(all_events[1], event_data_2); + assert_eq!(all_events[2], event_data_3); + + // Test empty events list for non-existent session + let non_existent_session = SessionId::new(99999); + let empty_events = conn.load_receiver_session_events(&non_existent_session); + assert_eq!(empty_events.len(), 0); + + // Test session with no events + let session_id_3 = conn.save_new_payjoin_receiver_session(); + let session_3 = SessionId::new(session_id_3); + let no_events = conn.load_receiver_session_events(&session_3); + assert_eq!(no_events.len(), 0); + + // Test completing multiple sessions + conn.update_receiver_session_completed_at(&session_3); + let final_active_sessions = conn.get_all_receiver_session_ids(); + assert_eq!(final_active_sessions.len(), 1); + assert!(final_active_sessions.iter().any(|s| s.0 == session_id_2)); + + fs::remove_dir_all(temp_dir).unwrap(); + } + + #[test] + fn test_payjoin_sender_sessions() { + let (temp_dir, _, _, db) = dummy_db(); + let mut conn = db.connection().unwrap(); + + let session_id_1 = conn.save_new_payjoin_sender_session(); + assert!(session_id_1 > 0); + let session_id_2 = conn.save_new_payjoin_sender_session(); + assert!(session_id_2 > session_id_1); + + let active_sessions = conn.get_all_sender_session_ids(); + assert_eq!(active_sessions.len(), 2); + assert!(active_sessions.iter().any(|s| s.0 == session_id_1)); + assert!(active_sessions.iter().any(|s| s.0 == session_id_2)); + + let event_data = b"test event data".to_vec(); + let session_id = SessionId::new(session_id_1); + conn.save_sender_session_event(&session_id, event_data.clone()); + + // Test load_sender_session_events + let events = conn.load_sender_session_events(&session_id); + assert_eq!(events.len(), 1); + assert_eq!(events[0], event_data); + + conn.update_sender_session_completed_at(&session_id); + // This should not change how events are loaded + let events = conn.load_sender_session_events(&session_id); + assert_eq!(events.len(), 1); + assert_eq!(events[0], event_data); + + // Verify session is no longer active + let active_sessions_after = conn.get_all_sender_session_ids(); + assert_eq!(active_sessions_after.len(), 1); + assert!(!active_sessions_after.iter().any(|s| s.0 == session_id_1)); + + let event_data_2 = b"second event data".to_vec(); + // Data is same as the second event + let event_data_3 = b"second event data".to_vec(); + conn.save_sender_session_event(&session_id, event_data_2.clone()); + conn.save_sender_session_event(&session_id, event_data_3.clone()); + + let all_events = conn.load_sender_session_events(&session_id); + assert_eq!(all_events.len(), 3); + assert_eq!(all_events[0], event_data); + assert_eq!(all_events[1], event_data_2); + assert_eq!(all_events[2], event_data_3); + + // Test empty events list for non-existent session + let non_existent_session = SessionId::new(99999); + let empty_events = conn.load_sender_session_events(&non_existent_session); + assert_eq!(empty_events.len(), 0); + + // Test session with no events + let session_id_3 = conn.save_new_payjoin_sender_session(); + let session_3 = SessionId::new(session_id_3); + let no_events = conn.load_sender_session_events(&session_3); + assert_eq!(no_events.len(), 0); + + // Test completing multiple sessions + conn.update_sender_session_completed_at(&session_3); + let final_active_sessions = conn.get_all_sender_session_ids(); + assert_eq!(final_active_sessions.len(), 1); + assert!(final_active_sessions.iter().any(|s| s.0 == session_id_2)); + + fs::remove_dir_all(temp_dir).unwrap(); + } } From 5b164668ffac26a68e30e1d36b21e48f29b04c9a Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 22 Jul 2025 14:49:00 -0400 Subject: [PATCH 37/72] Rename address struct payjoin-uri -> bip21 --- liana-gui/src/app/state/receive.rs | 4 ++-- liana-gui/src/services/connect/client/backend/mod.rs | 2 +- lianad/src/commands/mod.rs | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index 61d60b690..08852349f 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -214,7 +214,7 @@ impl State for ReceivePanel { daemon .get_new_address() .await - .map(|res| (res.address, res.derivation_index, res.payjoin_uri)) + .map(|res| (res.address, res.derivation_index, res.bip21)) .map_err(|e| e.into()) }, Message::ReceiveAddress, @@ -306,7 +306,7 @@ impl State for ReceivePanel { daemon .receive_payjoin() .await - .map(|res| (res.address, res.derivation_index, res.payjoin_uri)) + .map(|res| (res.address, res.derivation_index, res.bip21)) .map_err(|e| e.into()) }, Message::ReceiveAddress, diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index f16dfc339..d3697273c 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -620,7 +620,7 @@ impl Daemon for BackendWalletClient { Ok(GetAddressResult { address: res.address, derivation_index: res.derivation_index, - payjoin_uri: "bitcoin:".to_string(), + bip21: "bitcoin:".to_string(), }) } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 1fb4dc127..f6711dac2 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -1517,20 +1517,19 @@ pub struct GetAddressResult { #[serde(deserialize_with = "deser_addr_assume_checked")] pub address: bitcoin::Address, pub derivation_index: bip32::ChildNumber, - pub payjoin_uri: String, + pub bip21: String, } impl GetAddressResult { pub fn new( address: bitcoin::Address, derivation_index: bip32::ChildNumber, - // TODO: rename to bip21 - payjoin_uri: String, + bip21: String, ) -> Self { Self { address, derivation_index, - payjoin_uri, + bip21, } } } From 309ba07c10cd4c8651d8dd031d86c6685fd0f7b4 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 22 Jul 2025 15:43:41 -0400 Subject: [PATCH 38/72] Replace bip21 string with url type --- liana-gui/src/app/message.rs | 3 +- liana-gui/src/app/state/receive.rs | 15 ++++----- liana-gui/src/app/view/receive.rs | 31 ++++++++++--------- .../services/connect/client/backend/mod.rs | 2 +- lianad/src/commands/mod.rs | 11 ++++--- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index 62af89f62..b6b1d8c6e 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -7,6 +7,7 @@ use liana::miniscript::bitcoin::{ Address, Txid, }; use lianad::config::Config as DaemonConfig; +use payjoin::Url; use crate::{ app::{cache::Cache, error::Error, view, wallet::Wallet}, @@ -25,7 +26,7 @@ pub enum Message { DaemonConfigLoaded(Result<(), Error>), LoadWallet(Wallet), Info(Result), - ReceiveAddress(Result<(Address, ChildNumber, String), Error>), + ReceiveAddress(Result<(Address, ChildNumber, Option), Error>), /// Revealed addresses. The second element contains the start index used for the request. RevealedAddresses( Result, diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index 08852349f..eb69ac633 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -7,6 +7,7 @@ use liana::miniscript::bitcoin::{ Address, Network, }; use liana_ui::{component::modal, widget::*}; +use payjoin::Url; use crate::daemon::model::LabelsLoader; use crate::dir::LianaDirectory; @@ -39,7 +40,7 @@ pub enum Modal { #[derive(Debug, Default)] pub struct Addresses { list: Vec
, - payjoin_uris: HashMap, + bip21s: HashMap, derivation_indexes: Vec, labels: HashMap, } @@ -122,7 +123,7 @@ impl State for ReceivePanel { self.warning.as_ref(), view::receive::receive( &self.addresses.list, - &self.addresses.payjoin_uris, + &self.addresses.bip21s, &self.addresses.labels, &self.prev_addresses.list, &self.prev_addresses.labels, @@ -177,13 +178,13 @@ impl State for ReceivePanel { } Message::ReceiveAddress(res) => { match res { - Ok((address, derivation_index, payjoin_uri)) => { + Ok((address, derivation_index, bip21)) => { self.warning = None; self.addresses.list.push(address.clone()); self.addresses.derivation_indexes.push(derivation_index); - self.addresses - .payjoin_uris - .insert(address.to_string(), payjoin_uri); + if let Some(bip21) = bip21 { + self.addresses.bip21s.insert(address, bip21); + } } Err(e) => self.warning = Some(e), } @@ -503,7 +504,7 @@ mod tests { Ok(json!(GetAddressResult::new( addr.clone(), ChildNumber::from_normal_idx(0).unwrap(), - "".to_string(), + None, ))), ), ]); diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index ab9cbd459..b5d1d2f3d 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -23,6 +23,7 @@ use liana_ui::{ icon, theme, widget::*, }; +use payjoin::Url; use crate::{ app::{ @@ -37,13 +38,12 @@ use super::message::Message; fn address_card<'a>( row_index: usize, address: &'a bitcoin::Address, - maybe_payjoin_uri: Option<&String>, + maybe_bip21: Option<&Url>, labels: &'a HashMap, labels_editing: &'a HashMap>, ) -> Container<'a, Message> { let addr = address.to_string(); - let payjoin_uri = maybe_payjoin_uri.unwrap_or(&String::new()).clone(); - let has_payjoin_uri = !payjoin_uri.is_empty(); + let has_bip21 = maybe_bip21.is_some(); card::simple( Column::new() .push(if let Some(label) = labels_editing.get(&addr) { @@ -77,7 +77,7 @@ fn address_card<'a>( ) .align_y(Alignment::Center), ) - .push_maybe(has_payjoin_uri.then_some({ + .push_maybe(has_bip21.then_some({ Row::new() .push( Container::new( @@ -85,9 +85,11 @@ fn address_card<'a>( Column::new() .push(Space::with_height(Length::Fixed(10.0))) .push( - p2_regular(&payjoin_uri) - .small() - .style(theme::text::secondary), + p2_regular( + &maybe_bip21.expect("checked above").to_string(), + ) + .small() + .style(theme::text::secondary), ) // Space between the URI and the scrollbar .push(Space::with_height(Length::Fixed(10.0))), @@ -102,7 +104,9 @@ fn address_card<'a>( ) .push( Button::new(icon::clipboard_icon().style(theme::text::secondary)) - .on_press(Message::Clipboard(payjoin_uri.clone())) + .on_press(Message::Clipboard( + maybe_bip21.expect("checked above").to_string(), + )) .style(theme::button::transparent_border), ) .align_y(Alignment::Center) @@ -126,7 +130,7 @@ fn address_card<'a>( #[allow(clippy::too_many_arguments)] pub fn receive<'a>( addresses: &'a [bitcoin::Address], - payjoin_uris: &'a HashMap, + bip21s: &'a HashMap, labels: &'a HashMap, prev_addresses: &'a [bitcoin::Address], prev_labels: &'a HashMap, @@ -167,13 +171,12 @@ pub fn receive<'a>( // iterate starting from most recently generated Column::new().spacing(10).width(Length::Fill), |col, (i, address)| { - let addr = address.to_string(); - let maybe_payjoin_uri = payjoin_uris.get(&addr); + let maybe_bip21 = bip21s.get(address); addresses_count += 1; col.push(address_card( i, address, - maybe_payjoin_uri, + maybe_bip21, labels, labels_editing, )) @@ -210,7 +213,7 @@ pub fn receive<'a>( Column::new().spacing(10).width(Length::Fill), |col, (i, address)| { let addr = address.to_string(); - let maybe_payjoin_uri = payjoin_uris.get(&addr); + let maybe_bip21 = bip21s.get(address); col.push(if !selected.contains(address) { Button::new( Row::new() @@ -277,7 +280,7 @@ pub fn receive<'a>( Button::new(address_card( addresses_count + i, address, - maybe_payjoin_uri, + maybe_bip21, prev_labels, labels_editing, )) diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index d3697273c..be609c4b7 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -620,7 +620,7 @@ impl Daemon for BackendWalletClient { Ok(GetAddressResult { address: res.address, derivation_index: res.derivation_index, - bip21: "bitcoin:".to_string(), + bip21: None, }) } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index f6711dac2..ed359db8e 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -37,6 +37,7 @@ use std::{ collections::{hash_map, HashMap, HashSet}, convert::{TryFrom, TryInto}, fmt, + str::FromStr, sync::{self, mpsc, Arc}, time::SystemTime, }; @@ -53,7 +54,7 @@ use payjoin::{ bitcoin::{key::Secp256k1, FeeRate}, receive::v2::{replay_event_log as replay_receiver_event_log, Receiver, UninitializedReceiver}, send::v2::{replay_event_log as replay_sender_event_log, SenderBuilder}, - Uri, UriExt, + Uri, UriExt, Url, }; use serde::{Deserialize, Serialize}; @@ -378,7 +379,7 @@ impl DaemonControl { .receive_descriptor() .derive(new_index, &self.secp) .address(self.config.bitcoin_config.network); - GetAddressResult::new(address, new_index, "".to_string()) + GetAddressResult::new(address, new_index, None) } pub fn receive_payjoin(&self) -> Result { @@ -424,7 +425,7 @@ impl DaemonControl { Ok(GetAddressResult::new( address, new_index, - session.pj_uri().to_string(), + Some(Url::from_str(session.pj_uri().to_string().as_str()).expect("Should be valid")), )) } @@ -1517,14 +1518,14 @@ pub struct GetAddressResult { #[serde(deserialize_with = "deser_addr_assume_checked")] pub address: bitcoin::Address, pub derivation_index: bip32::ChildNumber, - pub bip21: String, + pub bip21: Option, } impl GetAddressResult { pub fn new( address: bitcoin::Address, derivation_index: bip32::ChildNumber, - bip21: String, + bip21: Option, ) -> Self { Self { address, From de8da922d62bc6453c58b2a4316f98df4348bd1d Mon Sep 17 00:00:00 2001 From: user Date: Wed, 23 Jul 2025 15:09:21 -0400 Subject: [PATCH 39/72] Add support for bip21 qr codes on the receiver This adds support for displaying bip21 qr codes to the receiver when initializing a payjoin. --- liana-gui/src/app/state/receive.rs | 38 ++++++++++++++++++++++++++++++ liana-gui/src/app/view/message.rs | 1 + liana-gui/src/app/view/receive.rs | 12 ++++++---- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/liana-gui/src/app/state/receive.rs b/liana-gui/src/app/state/receive.rs index eb69ac633..8622f9a8b 100644 --- a/liana-gui/src/app/state/receive.rs +++ b/liana-gui/src/app/state/receive.rs @@ -34,6 +34,7 @@ const PREV_ADDRESSES_PAGE_SIZE: usize = 20; pub enum Modal { VerifyAddress(VerifyAddressModal), ShowQrCode(ShowQrCodeModal), + ShowBip21QrCode(ShowBip21QrCodeModal), None, } @@ -142,6 +143,9 @@ impl State for ReceivePanel { Modal::ShowQrCode(m) => modal::Modal::new(content, m.view()) .on_blur(Some(view::Message::Close)) .into(), + Modal::ShowBip21QrCode(m) => modal::Modal::new(content, m.view()) + .on_blur(Some(view::Message::Close)) + .into(), Modal::None => content, } } @@ -300,6 +304,20 @@ impl State for ReceivePanel { } Task::none() } + Message::View(view::Message::ShowBip21QrCode(i)) => { + if let (Some(bip21), Some(index)) = ( + &self + .addresses + .bip21s + .get(self.address(i).expect("Address should be in bip21")), + self.derivation_index(i), + ) { + if let Some(modal) = ShowBip21QrCodeModal::new(bip21, *index) { + self.modal = Modal::ShowBip21QrCode(modal); + } + } + Task::none() + } Message::View(view::Message::PayjoinInitiate) => { let daemon = daemon.clone(); Task::perform( @@ -453,6 +471,26 @@ impl ShowQrCodeModal { } } +pub struct ShowBip21QrCodeModal { + qr_code: qr_code::Data, + bip21: String, +} + +impl ShowBip21QrCodeModal { + pub fn new(bip21: &payjoin::Url, _index: ChildNumber) -> Option { + qr_code::Data::new(format!("{}", bip21)) + .ok() + .map(|qr_code| Self { + qr_code, + bip21: bip21.to_string(), + }) + } + + fn view(&self) -> Element { + view::receive::qr_modal(&self.qr_code, &self.bip21) + } +} + async fn verify_address( hw: std::sync::Arc, index: ChildNumber, diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 8007c6e09..f2d1e9be1 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -26,6 +26,7 @@ pub enum Message { SelectHardwareWallet(usize), CreateRbf(CreateRbfMessage), ShowQrCode(usize), + ShowBip21QrCode(usize), ImportExport(ImportExportMessage), HideRescanWarning, ExportPsbt, diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index b5d1d2f3d..f06512d29 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -118,10 +118,13 @@ fn address_card<'a>( .on_press(Message::Select(row_index)), ) .push(Space::with_width(Length::Fill)) - .push( + .push(if has_bip21 { + button::secondary(None, "Show Bip21 QR Code") + .on_press(Message::ShowBip21QrCode(row_index)) + } else { button::secondary(None, "Show QR Code") - .on_press(Message::ShowQrCode(row_index)), - ), + .on_press(Message::ShowQrCode(row_index)) + }), ) .spacing(10), ) @@ -400,6 +403,7 @@ pub fn verify_address_modal<'a>( } pub fn qr_modal<'a>(qr: &'a qr_code::Data, address: &'a String) -> Element<'a, Message> { + let max_width = if address.len() > 64 { 600 } else { 400 }; Column::new() .push( Row::new() @@ -413,6 +417,6 @@ pub fn qr_modal<'a>(qr: &'a qr_code::Data, address: &'a String) -> Element<'a, M .push(Space::with_height(Length::Fixed(15.0))) .push(Container::new(text(address).size(15)).center_x(Length::Fill)) .width(Length::Fill) - .max_width(400) + .max_width(max_width) .into() } From dc9aefd860e78c1f32449aa15ee8c6f6fc5bc30b Mon Sep 17 00:00:00 2001 From: user Date: Mon, 14 Jul 2025 15:26:41 -0400 Subject: [PATCH 40/72] [WIP] add somre receiver state checks Directly reference data made available within liana and do not actually use the closure that calls out to rust-payjoin. --- lianad/src/payjoin/receiver.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 1175f7e25..6db03e643 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -4,11 +4,12 @@ use std::{ sync::{self, Arc}, }; -use liana::descriptors; +use liana::{descriptors, spend::AddrInfo}; use payjoin::{ bitcoin::{ - consensus::encode::serialize_hex, psbt::Input, secp256k1, OutPoint, Sequence, TxIn, Weight, + self, consensus::encode::serialize_hex, psbt::Input, secp256k1, OutPoint, Sequence, TxIn, + Weight, }, persist::OptionalTransitionOutcome, receive::{ @@ -89,7 +90,13 @@ fn check_inputs_not_owned( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal - .check_inputs_not_owned(|_| Ok(false)) + .check_inputs_not_owned(|script| { + let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); + Ok(db_conn + .derivation_index_by_address(&address) + .map(|(index, is_change)| AddrInfo { index, is_change }) + .is_some()) + }) .save(persister)?; check_no_inputs_seen_before(proposal, persister, db_conn, desc, secp) } @@ -102,6 +109,8 @@ fn check_no_inputs_seen_before( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal + // TODO implement check_no_inputs_seen_before callback and add new table to mark relevant + // outpoint as seen for the future .check_no_inputs_seen_before(|_| Ok(false)) .save(persister)?; identify_receiver_outputs(proposal, persister, db_conn, desc, secp) @@ -115,7 +124,13 @@ fn identify_receiver_outputs( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal - .identify_receiver_outputs(|_| Ok(true)) + .identify_receiver_outputs(|script| { + let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); + Ok(db_conn + .derivation_index_by_address(&address) + .map(|(index, is_change)| AddrInfo { index, is_change }) + .is_some()) + }) .save(persister)?; commit_outputs(proposal, persister, db_conn, desc, secp) } From a99c8da1c8cdba6be907a5a90acd580741ac9043 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 15:03:14 -0400 Subject: [PATCH 41/72] Receiver closure should be mutable refrences --- lianad/src/payjoin/receiver.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 6db03e643..fd732b386 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -90,7 +90,7 @@ fn check_inputs_not_owned( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal - .check_inputs_not_owned(|script| { + .check_inputs_not_owned(&mut |script| { let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); Ok(db_conn .derivation_index_by_address(&address) @@ -111,7 +111,7 @@ fn check_no_inputs_seen_before( let proposal = proposal // TODO implement check_no_inputs_seen_before callback and add new table to mark relevant // outpoint as seen for the future - .check_no_inputs_seen_before(|_| Ok(false)) + .check_no_inputs_seen_before(&mut |_| Ok(false)) .save(persister)?; identify_receiver_outputs(proposal, persister, db_conn, desc, secp) } @@ -124,7 +124,7 @@ fn identify_receiver_outputs( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal - .identify_receiver_outputs(|script| { + .identify_receiver_outputs(&mut |script| { let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); Ok(db_conn .derivation_index_by_address(&address) From 18b597bec9109bf5ee06c441893002047ba9097a Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 14:27:59 -0400 Subject: [PATCH 42/72] Return errors from p2wsh hot signer --- liana/src/signer.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/liana/src/signer.rs b/liana/src/signer.rs index a996a28ac..fca8a19d5 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -378,16 +378,14 @@ impl HotSigner { // Sign each input in the PSBT. for i in 0..psbt.inputs.len() { if psbt.inputs[i].witness_script.is_some() { - match self.sign_p2wsh( + self.sign_p2wsh( secp, &mut sighash_cache, master_fingerprint, &mut psbt.inputs[i], i, - ) { - Ok(_) => log::info!("Signed input at {}", i), - Err(err) => log::warn!("Didnt sign input at {} | {}", i, err), - } + )?; + log::info!("Signed input at {}", i); } else { self.sign_taproot( secp, From 429d16f6aef17e425272f46484f774495d2ee00d Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 10:35:35 -0400 Subject: [PATCH 43/72] Breakout payjoin status from bip21 --- liana-gui/src/app/state/psbt.rs | 5 +-- liana-gui/src/app/state/spend/step.rs | 31 +++++++++++-------- liana-gui/src/app/view/psbt.rs | 4 +-- liana-gui/src/daemon/mod.rs | 5 ++- liana-gui/src/daemon/model.rs | 14 ++++++--- .../services/connect/client/backend/mod.rs | 2 ++ 6 files changed, 38 insertions(+), 23 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 9413251b5..7e6065808 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -194,12 +194,13 @@ impl PsbtState { Message::View(view::Message::Spend(view::SpendTxMessage::PayjoinInitiated)) => { self.tx.status = SpendStatus::PayjoinInitiated; self.modal = None; - if let Some(payjoin_info) = self.tx.payjoin_info.clone() { + if let Some(_payjoin_info) = self.tx.payjoin_status.clone() { let psbt = self.tx.psbt.clone(); + let bip21 = self.tx.bip21.clone(); return Task::perform( async move { daemon - .send_payjoin(payjoin_info.bip21, &psbt) + .send_payjoin(bip21, &psbt) .await .map_err(|e| e.into()) }, diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 4744c75c8..699714039 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -18,10 +18,7 @@ use liana::{ }, spend::{SpendCreationError, MAX_FEERATE}, }; -use lianad::{ - commands::ListCoinsEntry, - payjoin::types::{PayjoinInfo, PayjoinStatus}, -}; +use lianad::{commands::ListCoinsEntry, payjoin::types::PayjoinStatus}; use liana_ui::{component::form, widget::Element}; use payjoin::Uri; @@ -991,15 +988,22 @@ impl Step for SaveSpend { fn load(&mut self, _coins: &[Coin], _tip_height: i32, draft: &TransactionDraft) { let (psbt, warnings) = draft.generated.clone().unwrap(); - let bip21 = draft.recipients.first().unwrap().bip21.value.clone(); - let payjoin_info = if bip21.is_empty() { - None + let bip21 = draft + .recipients + .first() + .expect("one recipient") + .bip21 + .value + .clone(); + + let payjoin_status = if let Ok(uri) = Uri::try_from(bip21.as_str()) { + if uri.assume_checked().extras.pj_is_supported() { + Some(PayjoinStatus::Pending) + } else { + None + } } else { - // TODO: this seems wrong. The presence of a bip21 doesnt mean its a payjoin - Some(PayjoinInfo { - bip21, - status: PayjoinStatus::Pending, - }) + None }; let mut tx = SpendTx::new( @@ -1009,7 +1013,8 @@ impl Step for SaveSpend { &self.wallet.main_descriptor, &self.curve, draft.network, - payjoin_info, + payjoin_status, + bip21, ); tx.labels.clone_from(&draft.labels); diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index 824773d43..ebccab292 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -424,8 +424,8 @@ pub fn spend_overview_view<'a>( ) }) .push_maybe(if tx.path_ready().is_some() { - if let Some(payjoin_info) = &tx.payjoin_info { - if payjoin_info.status == PayjoinStatus::Pending { + if let Some(payjoin_status) = &tx.payjoin_status { + if *payjoin_status == PayjoinStatus::Pending { Some( button::secondary(None, "Send Payjoin") .on_press(Message::Spend(SpendTxMessage::SendPayjoin)) diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index ef7c00e80..a684aeb50 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -223,7 +223,10 @@ pub trait Daemon: Debug { &info.descriptors.main, &curve, info.network, - payjoin_info, + payjoin_info.as_ref().map(|info| info.status), + payjoin_info + .map(|info| info.bip21) + .unwrap_or("".to_string()), )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 431e12205..3710dffea 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -15,7 +15,7 @@ pub use lianad::commands::{ ListCoinsResult, ListRevealedAddressesEntry, ListRevealedAddressesResult, ListSpendEntry, ListSpendResult, ListTransactionsResult, TransactionInfo, }; -use lianad::payjoin::types::PayjoinInfo; +use lianad::payjoin::types::PayjoinStatus; pub type Coin = ListCoinsEntry; @@ -54,7 +54,9 @@ pub struct SpendTx { pub sigs: PartialSpendInfo, pub updated_at: Option, pub kind: TransactionKind, - pub payjoin_info: Option, + pub payjoin_status: Option, + // TODO: use a stronger type like bitcoin_uri + pub bip21: String, } #[derive(PartialOrd, Ord, Debug, Clone, PartialEq, Eq)] @@ -75,7 +77,8 @@ impl SpendTx { desc: &LianaDescriptor, secp: &secp256k1::Secp256k1, network: Network, - payjoin_info: Option, + payjoin_status: Option, + bip21: String, ) -> Self { // Use primary path if no inputs are using a relative locktime. let use_primary_path = !psbt @@ -150,7 +153,7 @@ impl SpendTx { }; // One input coin is missing, the psbt is deprecated for now. - if coins_map.len() != psbt.inputs.len() && payjoin_info.is_none() { + if coins_map.len() != psbt.inputs.len() && payjoin_status.is_none() { status = SpendStatus::Deprecated } @@ -195,7 +198,8 @@ impl SpendTx { status, sigs, network, - payjoin_info, + payjoin_status, + bip21, } } diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index be609c4b7..d58a35a1a 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -1311,7 +1311,9 @@ fn spend_tx_from_api( desc, secp, network, + // TODO: Payjoin status is getting set to None and so is bip21 None, + "".to_string(), // BIP21 ); tx.load_labels(&labels); tx From d6cc409c923bf75c502f30928a08a0de7e2d5514 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 11:05:56 -0400 Subject: [PATCH 44/72] Get payjoin info if spend is original proposal or ready to be signed psbt --- liana-gui/src/daemon/mod.rs | 1 + lianad/src/commands/mod.rs | 35 ++++++++++++++++++++++++----------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index a684aeb50..09098a3b6 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -215,6 +215,7 @@ pub trait Daemon: Debug { let payjoin_info = self .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) .await?; + log::info!("payjoin_info: {:?}", payjoin_info); spend_txs.push(model::SpendTx::new( tx.updated_at, diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index ed359db8e..bb79568c7 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -464,23 +464,36 @@ impl DaemonControl { txid: &bitcoin::Txid, ) -> Result, CommandError> { let mut db_conn = self.db.connection(); - + info!("Getting payjoin info for txid: {:?}", txid); for session_id in db_conn.get_all_receiver_session_ids() { let persister = ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); let (state, history) = replay_receiver_event_log(&persister).unwrap(); let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); + info!("Original txid {:?}", original_txid); + let ready_to_sign_txid = history + .psbt_ready_for_signing() + .map(|psbt| psbt.unsigned_tx.compute_txid()); + info!("Ready to sign txid {:?}", ready_to_sign_txid); + let bip21 = history + .pj_uri() + .expect("should exist at this point") + .to_string(); + if let Some(ready_to_sign_txid) = ready_to_sign_txid { + if ready_to_sign_txid == *txid { + return Ok(Some(PayjoinInfo { + bip21, + status: state.into(), + })); + } + } if let Some(original_txid) = original_txid { - // if original_txid == *txid { - let bip21 = history - .pj_uri() - .expect("should exist at this point") - .to_string(); - return Ok(Some(PayjoinInfo { - bip21, - status: state.into(), - })); - // } + if original_txid == *txid { + return Ok(Some(PayjoinInfo { + bip21, + status: state.into(), + })); + } } } From 1f0cf932be4f3396506abae31396daf1a334b2eb Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 11:10:58 -0400 Subject: [PATCH 45/72] Rename get all session ids to get all active session ids --- lianad/src/commands/mod.rs | 4 ++-- lianad/src/database/mod.rs | 12 ++++++------ lianad/src/database/sqlite/mod.rs | 17 +++++++++-------- lianad/src/payjoin/receiver.rs | 2 +- lianad/src/payjoin/sender.rs | 2 +- lianad/src/testutils.rs | 4 ++-- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index bb79568c7..76427b5ac 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -465,7 +465,7 @@ impl DaemonControl { ) -> Result, CommandError> { let mut db_conn = self.db.connection(); info!("Getting payjoin info for txid: {:?}", txid); - for session_id in db_conn.get_all_receiver_session_ids() { + for session_id in db_conn.get_all_active_receiver_session_ids() { let persister = ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); let (state, history) = replay_receiver_event_log(&persister).unwrap(); @@ -497,7 +497,7 @@ impl DaemonControl { } } - for session_id in db_conn.get_all_sender_session_ids() { + for session_id in db_conn.get_all_active_sender_session_ids() { log::info!("Checking sender session: {:?}", session_id); let persister = SenderPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); let (state, history) = replay_sender_event_log(&persister).unwrap(); diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 0cf5f2ef7..d85d45741 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -207,7 +207,7 @@ pub trait DatabaseConnection { fn save_new_payjoin_receiver_session(&mut self) -> i64; /// Get all Receiver Sessions - fn get_all_receiver_session_ids(&mut self) -> Vec; + fn get_all_active_receiver_session_ids(&mut self) -> Vec; /// Save a Receiver Session Event fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec); @@ -222,7 +222,7 @@ pub trait DatabaseConnection { /// Create a payjoin sender fn save_new_payjoin_sender_session(&mut self) -> i64; /// Get a all active payjoin senders - fn get_all_sender_session_ids(&mut self) -> Vec; + fn get_all_active_sender_session_ids(&mut self) -> Vec; /// Save a sender session event fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec); @@ -467,8 +467,8 @@ impl DatabaseConnection for SqliteConn { self.save_new_payjoin_receiver_session() } - fn get_all_receiver_session_ids(&mut self) -> Vec { - self.get_all_receiver_session_ids() + fn get_all_active_receiver_session_ids(&mut self) -> Vec { + self.get_all_active_receiver_session_ids() } fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec) { @@ -487,8 +487,8 @@ impl DatabaseConnection for SqliteConn { self.save_new_payjoin_sender_session() } - fn get_all_sender_session_ids(&mut self) -> Vec { - self.get_all_sender_session_ids() + fn get_all_active_sender_session_ids(&mut self) -> Vec { + self.get_all_active_sender_session_ids() } fn save_sender_session_event(&mut self, session_id: &SessionId, event: Vec) { diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 7df874021..2cf396c72 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -1034,7 +1034,7 @@ impl SqliteConn { } /// Get all active receiver session ids - pub fn get_all_receiver_session_ids(&mut self) -> Vec { + pub fn get_all_active_receiver_session_ids(&mut self) -> Vec { db_query( &mut self.conn, "SELECT id FROM payjoin_receivers WHERE completed_at IS NULL", @@ -1099,7 +1099,8 @@ impl SqliteConn { id } - pub fn get_all_sender_session_ids(&mut self) -> Vec { + /// Get all active sender session ids + pub fn get_all_active_sender_session_ids(&mut self) -> Vec { db_query( &mut self.conn, "SELECT id FROM payjoin_senders WHERE completed_at IS NULL", @@ -3764,7 +3765,7 @@ CREATE TABLE labels ( let session_id_2 = conn.save_new_payjoin_receiver_session(); assert!(session_id_2 > session_id_1); - let active_sessions = conn.get_all_receiver_session_ids(); + let active_sessions = conn.get_all_active_receiver_session_ids(); assert_eq!(active_sessions.len(), 2); assert!(active_sessions.iter().any(|s| s.0 == session_id_1)); assert!(active_sessions.iter().any(|s| s.0 == session_id_2)); @@ -3784,7 +3785,7 @@ CREATE TABLE labels ( assert_eq!(events[0], event_data); // Verify session is no longer active - let active_sessions_after = conn.get_all_receiver_session_ids(); + let active_sessions_after = conn.get_all_active_receiver_session_ids(); assert_eq!(active_sessions_after.len(), 1); assert!(!active_sessions_after.iter().any(|s| s.0 == session_id_1)); @@ -3813,7 +3814,7 @@ CREATE TABLE labels ( // Test completing multiple sessions conn.update_receiver_session_completed_at(&session_3); - let final_active_sessions = conn.get_all_receiver_session_ids(); + let final_active_sessions = conn.get_all_active_receiver_session_ids(); assert_eq!(final_active_sessions.len(), 1); assert!(final_active_sessions.iter().any(|s| s.0 == session_id_2)); @@ -3830,7 +3831,7 @@ CREATE TABLE labels ( let session_id_2 = conn.save_new_payjoin_sender_session(); assert!(session_id_2 > session_id_1); - let active_sessions = conn.get_all_sender_session_ids(); + let active_sessions = conn.get_all_active_sender_session_ids(); assert_eq!(active_sessions.len(), 2); assert!(active_sessions.iter().any(|s| s.0 == session_id_1)); assert!(active_sessions.iter().any(|s| s.0 == session_id_2)); @@ -3851,7 +3852,7 @@ CREATE TABLE labels ( assert_eq!(events[0], event_data); // Verify session is no longer active - let active_sessions_after = conn.get_all_sender_session_ids(); + let active_sessions_after = conn.get_all_active_sender_session_ids(); assert_eq!(active_sessions_after.len(), 1); assert!(!active_sessions_after.iter().any(|s| s.0 == session_id_1)); @@ -3880,7 +3881,7 @@ CREATE TABLE labels ( // Test completing multiple sessions conn.update_sender_session_completed_at(&session_3); - let final_active_sessions = conn.get_all_sender_session_ids(); + let final_active_sessions = conn.get_all_active_sender_session_ids(); assert_eq!(final_active_sessions.len(), 1); assert!(final_active_sessions.iter().any(|s| s.0 == session_id_2)); diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index fd732b386..e1303d686 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -310,7 +310,7 @@ fn process_receiver_session( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let mut db_conn = db.connection(); - for session_id in db_conn.get_all_receiver_session_ids() { + for session_id in db_conn.get_all_active_receiver_session_ids() { let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()); let (state, _) = replay_event_log(&persister) diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 8b6f8ff2c..2a359b8a4 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -91,7 +91,7 @@ fn process_sender_session( pub fn payjoin_sender_check(db: &sync::Arc>) { let mut db_conn = db.connection(); - for session_id in db_conn.get_all_sender_session_ids() { + for session_id in db_conn.get_all_active_sender_session_ids() { let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()); let (state, session_history) = replay_event_log(&persister) diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 4c801ab98..913d691d2 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -578,7 +578,7 @@ impl DatabaseConnection for DummyDatabase { todo!() } - fn get_all_receiver_session_ids(&mut self) -> Vec { + fn get_all_active_receiver_session_ids(&mut self) -> Vec { self.db .read() .expect("lock should not be poisoned") @@ -603,7 +603,7 @@ impl DatabaseConnection for DummyDatabase { id } - fn get_all_sender_session_ids(&mut self) -> Vec { + fn get_all_active_sender_session_ids(&mut self) -> Vec { self.db .read() .expect("lock should not be poisoned") From d560626c8c61a11581098e161f002596d4fff94f Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 12:30:32 -0400 Subject: [PATCH 46/72] Return only payjoin status from json rpc endpoint --- liana-gui/src/app/view/receive.rs | 14 ++++------- liana-gui/src/daemon/client/mod.rs | 4 ++-- liana-gui/src/daemon/embedded.rs | 4 ++-- liana-gui/src/daemon/mod.rs | 14 +++++------ .../services/connect/client/backend/mod.rs | 4 ++-- lianad/src/commands/mod.rs | 24 +++++-------------- lianad/src/payjoin/types.rs | 1 + 7 files changed, 24 insertions(+), 41 deletions(-) diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index f06512d29..b1e00937f 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -77,7 +77,7 @@ fn address_card<'a>( ) .align_y(Alignment::Center), ) - .push_maybe(has_bip21.then_some({ + .push_maybe(maybe_bip21.map(|bip21| { Row::new() .push( Container::new( @@ -85,11 +85,9 @@ fn address_card<'a>( Column::new() .push(Space::with_height(Length::Fixed(10.0))) .push( - p2_regular( - &maybe_bip21.expect("checked above").to_string(), - ) - .small() - .style(theme::text::secondary), + p2_regular(&bip21.to_string()) + .small() + .style(theme::text::secondary), ) // Space between the URI and the scrollbar .push(Space::with_height(Length::Fixed(10.0))), @@ -104,9 +102,7 @@ fn address_card<'a>( ) .push( Button::new(icon::clipboard_icon().style(theme::text::secondary)) - .on_press(Message::Clipboard( - maybe_bip21.expect("checked above").to_string(), - )) + .on_press(Message::Clipboard(bip21.to_string())) .style(theme::button::transparent_border), ) .align_y(Alignment::Center) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 50cd30c71..111373021 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,7 +5,7 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; -use lianad::payjoin::types::PayjoinInfo; +use lianad::payjoin::types::{PayjoinInfo, PayjoinStatus}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -118,7 +118,7 @@ impl Daemon for Lianad { Ok(()) } - async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { + async fn get_payjoin_info(&self, txid: &Txid) -> Result { self.call("getpayjoininfo", Some(vec![txid.to_string()])) } diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 4170fccfb..d128022e5 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,6 +1,6 @@ -use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; use lianad::payjoin::types::PayjoinInfo; +use lianad::{bip329::Labels, payjoin::types::PayjoinStatus}; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; @@ -139,7 +139,7 @@ impl Daemon for EmbeddedDaemon { .await } - async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError> { + async fn get_payjoin_info(&self, txid: &Txid) -> Result { self.command(|daemon| { daemon .get_payjoin_info(txid) diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 09098a3b6..e6f52b060 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -18,7 +18,7 @@ use liana::miniscript::bitcoin::{ }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; -use lianad::payjoin::types::PayjoinInfo; +use lianad::payjoin::types::{PayjoinInfo, PayjoinStatus}; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -117,7 +117,7 @@ pub trait Daemon: Debug { ) -> Result; async fn receive_payjoin(&self) -> Result; async fn send_payjoin(&self, bip21: String, psbt: &Psbt) -> Result<(), DaemonError>; - async fn get_payjoin_info(&self, txid: &Txid) -> Result, DaemonError>; + async fn get_payjoin_info(&self, txid: &Txid) -> Result; async fn update_deriv_indexes( &self, receive: Option, @@ -212,10 +212,9 @@ pub trait Daemon: Debug { .cloned() .collect(); - let payjoin_info = self + let payjoin_status = self .get_payjoin_info(&tx.psbt.unsigned_tx.compute_txid()) .await?; - log::info!("payjoin_info: {:?}", payjoin_info); spend_txs.push(model::SpendTx::new( tx.updated_at, @@ -224,10 +223,9 @@ pub trait Daemon: Debug { &info.descriptors.main, &curve, info.network, - payjoin_info.as_ref().map(|info| info.status), - payjoin_info - .map(|info| info.bip21) - .unwrap_or("".to_string()), + Some(payjoin_status), + // TODO: should spend tx even have a bip21 field here. This feels like a UI concern + "TODO: dont have bip21 in this context".to_string(), )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index d58a35a1a..4bad1f5d3 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -17,7 +17,7 @@ use lianad::{ bip329::Labels, commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, - payjoin::types::PayjoinInfo, + payjoin::types::{PayjoinInfo, PayjoinStatus}, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; @@ -632,7 +632,7 @@ impl Daemon for BackendWalletClient { unimplemented!() } - async fn get_payjoin_info(&self, _txid: &Txid) -> Result, DaemonError> { + async fn get_payjoin_info(&self, _txid: &Txid) -> Result { unimplemented!() } diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 76427b5ac..9a38f44e0 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -11,7 +11,7 @@ use crate::{ payjoin::{ db::{ReceiverPersister, SenderPersister}, helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, - types::PayjoinInfo, + types::{PayjoinInfo, PayjoinStatus}, }, poller::PollerMessage, DaemonControl, VERSION, @@ -459,10 +459,7 @@ impl DaemonControl { } /// Get Payjoin URI (BIP21) and its sender/receiver status by txid - pub fn get_payjoin_info( - &self, - txid: &bitcoin::Txid, - ) -> Result, CommandError> { + pub fn get_payjoin_info(&self, txid: &bitcoin::Txid) -> Result { let mut db_conn = self.db.connection(); info!("Getting payjoin info for txid: {:?}", txid); for session_id in db_conn.get_all_active_receiver_session_ids() { @@ -481,18 +478,12 @@ impl DaemonControl { .to_string(); if let Some(ready_to_sign_txid) = ready_to_sign_txid { if ready_to_sign_txid == *txid { - return Ok(Some(PayjoinInfo { - bip21, - status: state.into(), - })); + return Ok(state.into()); } } if let Some(original_txid) = original_txid { if original_txid == *txid { - return Ok(Some(PayjoinInfo { - bip21, - status: state.into(), - })); + return Ok(state.into()); } } } @@ -510,15 +501,12 @@ impl DaemonControl { .endpoint() .expect("should exist at this point") .to_string(); - return Ok(Some(PayjoinInfo { - bip21, - status: state.into(), - })); + return Ok(state.into()); // } } } - Ok(None) + Ok(PayjoinStatus::Unknown) } /// Update derivation indexes diff --git a/lianad/src/payjoin/types.rs b/lianad/src/payjoin/types.rs index c75663123..ae6fead08 100644 --- a/lianad/src/payjoin/types.rs +++ b/lianad/src/payjoin/types.rs @@ -7,6 +7,7 @@ pub enum PayjoinStatus { WaitingToSign, Success, Failed, + Unknown, } impl From for PayjoinStatus { From d74a605b3f497d000a6bc6ac415a863a8925d659 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 13:04:56 -0400 Subject: [PATCH 47/72] Seperate out bip21 from spendtx model --- liana-gui/src/app/state/psbt.rs | 7 +++++-- liana-gui/src/app/state/psbts.rs | 7 ++++--- liana-gui/src/app/state/spend/step.rs | 8 ++++++-- liana-gui/src/app/view/receive.rs | 1 - liana-gui/src/daemon/client/mod.rs | 2 +- liana-gui/src/daemon/embedded.rs | 1 - liana-gui/src/daemon/mod.rs | 4 +--- liana-gui/src/daemon/model.rs | 6 ++---- liana-gui/src/services/connect/client/backend/mod.rs | 1 - 9 files changed, 19 insertions(+), 18 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index 7e6065808..eedffdddd 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -91,6 +91,7 @@ pub struct PsbtState { pub wallet: Arc, pub desc_policy: LianaPolicy, pub tx: SpendTx, + pub bip21: Option, pub saved: bool, pub warning: Option, pub labels_edited: LabelsEdited, @@ -98,7 +99,7 @@ pub struct PsbtState { } impl PsbtState { - pub fn new(wallet: Arc, tx: SpendTx, saved: bool) -> Self { + pub fn new(wallet: Arc, tx: SpendTx, saved: bool, bip21: Option) -> Self { Self { desc_policy: wallet.main_descriptor.policy(), wallet, @@ -106,6 +107,7 @@ impl PsbtState { warning: None, modal: None, tx, + bip21, saved, } } @@ -196,7 +198,8 @@ impl PsbtState { self.modal = None; if let Some(_payjoin_info) = self.tx.payjoin_status.clone() { let psbt = self.tx.psbt.clone(); - let bip21 = self.tx.bip21.clone(); + // TODO: should this be an error? + let bip21 = self.bip21.clone().expect("bip21 should be set"); return Task::perform( async move { daemon diff --git a/liana-gui/src/app/state/psbts.rs b/liana-gui/src/app/state/psbts.rs index e6781e54e..0437d1378 100644 --- a/liana-gui/src/app/state/psbts.rs +++ b/liana-gui/src/app/state/psbts.rs @@ -31,7 +31,7 @@ impl PsbtsPanel { } pub fn preselect(&mut self, spend_tx: SpendTx) { - let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true); + let psbt_state = psbt::PsbtState::new(self.wallet.clone(), spend_tx, true, None); self.selected_tx = Some(psbt_state); self.warning = None; self.modal = None; @@ -81,7 +81,8 @@ impl State for PsbtsPanel { spend_tx.psbt.unsigned_tx.compute_txid() == tx.tx.psbt.unsigned_tx.compute_txid() }) { - let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true); + let tx = + psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true, None); let cmd = tx.load(daemon); self.selected_tx = Some(tx); return cmd; @@ -119,7 +120,7 @@ impl State for PsbtsPanel { } Message::View(view::Message::Select(i)) => { if let Some(tx) = self.spend_txs.get(i) { - let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true); + let tx = psbt::PsbtState::new(self.wallet.clone(), tx.clone(), true, None); let cmd = tx.load(daemon); self.selected_tx = Some(tx); return cmd; diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index 699714039..ff37c9a19 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -1014,7 +1014,6 @@ impl Step for SaveSpend { &self.curve, draft.network, payjoin_status, - bip21, ); tx.labels.clone_from(&draft.labels); @@ -1034,7 +1033,12 @@ impl Step for SaveSpend { } self.spend = Some(( - psbt::PsbtState::new(self.wallet.clone(), tx, false), + psbt::PsbtState::new( + self.wallet.clone(), + tx, + false, + if bip21.is_empty() { None } else { Some(bip21) }, + ), warnings, )); } diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index b1e00937f..75d085089 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -43,7 +43,6 @@ fn address_card<'a>( labels_editing: &'a HashMap>, ) -> Container<'a, Message> { let addr = address.to_string(); - let has_bip21 = maybe_bip21.is_some(); card::simple( Column::new() .push(if let Some(label) = labels_editing.get(&addr) { diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 111373021..3f48f3721 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,7 +5,7 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; -use lianad::payjoin::types::{PayjoinInfo, PayjoinStatus}; +use lianad::payjoin::types::{PayjoinStatus}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index d128022e5..611f32191 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,5 +1,4 @@ use lianad::commands::UpdateDerivIndexesResult; -use lianad::payjoin::types::PayjoinInfo; use lianad::{bip329::Labels, payjoin::types::PayjoinStatus}; use std::collections::{HashMap, HashSet}; use tokio::sync::Mutex; diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index e6f52b060..91d6c2c21 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -18,7 +18,7 @@ use liana::miniscript::bitcoin::{ }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; -use lianad::payjoin::types::{PayjoinInfo, PayjoinStatus}; +use lianad::payjoin::types::{PayjoinStatus}; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, @@ -224,8 +224,6 @@ pub trait Daemon: Debug { &curve, info.network, Some(payjoin_status), - // TODO: should spend tx even have a bip21 field here. This feels like a UI concern - "TODO: dont have bip21 in this context".to_string(), )); } load_labels(self, &mut spend_txs).await?; diff --git a/liana-gui/src/daemon/model.rs b/liana-gui/src/daemon/model.rs index 3710dffea..b70fd6bbd 100644 --- a/liana-gui/src/daemon/model.rs +++ b/liana-gui/src/daemon/model.rs @@ -55,8 +55,8 @@ pub struct SpendTx { pub updated_at: Option, pub kind: TransactionKind, pub payjoin_status: Option, - // TODO: use a stronger type like bitcoin_uri - pub bip21: String, + // // TODO: use a stronger type like bitcoin_uri + // pub bip21: String, } #[derive(PartialOrd, Ord, Debug, Clone, PartialEq, Eq)] @@ -78,7 +78,6 @@ impl SpendTx { secp: &secp256k1::Secp256k1, network: Network, payjoin_status: Option, - bip21: String, ) -> Self { // Use primary path if no inputs are using a relative locktime. let use_primary_path = !psbt @@ -199,7 +198,6 @@ impl SpendTx { sigs, network, payjoin_status, - bip21, } } diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index 4bad1f5d3..9ef1ba9f5 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -1313,7 +1313,6 @@ fn spend_tx_from_api( network, // TODO: Payjoin status is getting set to None and so is bip21 None, - "".to_string(), // BIP21 ); tx.load_labels(&labels); tx From 6167b0f296bf90635fd8583f4d753f05cb82957e Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 24 Jul 2025 13:18:09 -0400 Subject: [PATCH 48/72] Clean up unused code in get payjoin info --- liana-gui/src/daemon/client/mod.rs | 2 +- liana-gui/src/daemon/mod.rs | 2 +- liana-gui/src/services/connect/client/backend/mod.rs | 2 +- lianad/src/commands/mod.rs | 11 ++--------- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index 3f48f3721..86ab65cd4 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -5,7 +5,7 @@ use std::iter::FromIterator; use async_trait::async_trait; use lianad::bip329::Labels; use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; -use lianad::payjoin::types::{PayjoinStatus}; +use lianad::payjoin::types::PayjoinStatus; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 91d6c2c21..16a093156 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -18,7 +18,7 @@ use liana::miniscript::bitcoin::{ }; use lianad::bip329::Labels; use lianad::commands::UpdateDerivIndexesResult; -use lianad::payjoin::types::{PayjoinStatus}; +use lianad::payjoin::types::PayjoinStatus; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, diff --git a/liana-gui/src/services/connect/client/backend/mod.rs b/liana-gui/src/services/connect/client/backend/mod.rs index 9ef1ba9f5..cdd92c656 100644 --- a/liana-gui/src/services/connect/client/backend/mod.rs +++ b/liana-gui/src/services/connect/client/backend/mod.rs @@ -17,7 +17,7 @@ use lianad::{ bip329::Labels, commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, - payjoin::types::{PayjoinInfo, PayjoinStatus}, + payjoin::types::PayjoinStatus, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; use tokio::sync::RwLock; diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 9a38f44e0..449311389 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -472,10 +472,6 @@ impl DaemonControl { .psbt_ready_for_signing() .map(|psbt| psbt.unsigned_tx.compute_txid()); info!("Ready to sign txid {:?}", ready_to_sign_txid); - let bip21 = history - .pj_uri() - .expect("should exist at this point") - .to_string(); if let Some(ready_to_sign_txid) = ready_to_sign_txid { if ready_to_sign_txid == *txid { return Ok(state.into()); @@ -494,13 +490,10 @@ impl DaemonControl { let (state, history) = replay_sender_event_log(&persister).unwrap(); log::info!("Sender state: {:?}", state); let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); - if let Some(original_txid) = original_txid { + if let Some(_original_txid) = original_txid { + // TODO: fix this // if original_txid == *txid { // TODO: this isnt a bip21, but a payjoin endpoint. Does this need to get returned? - let bip21 = history - .endpoint() - .expect("should exist at this point") - .to_string(); return Ok(state.into()); // } } From e03b9d14b9cb2614285ad547c99f215da6282430 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 25 Jul 2025 12:55:04 -0400 Subject: [PATCH 49/72] Store original and proposed txids for pj sender --- lianad/src/commands/mod.rs | 18 ++++------ lianad/src/database/mod.rs | 24 +++++++++++-- lianad/src/database/sqlite/mod.rs | 50 ++++++++++++++++++++++++---- lianad/src/database/sqlite/schema.rs | 2 ++ lianad/src/payjoin/db.rs | 5 +-- lianad/src/payjoin/sender.rs | 2 +- 6 files changed, 77 insertions(+), 24 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 449311389..2d240e3b5 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -11,7 +11,7 @@ use crate::{ payjoin::{ db::{ReceiverPersister, SenderPersister}, helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, - types::{PayjoinInfo, PayjoinStatus}, + types::PayjoinStatus, }, poller::PollerMessage, DaemonControl, VERSION, @@ -447,8 +447,9 @@ impl DaemonControl { psbt.finalize_mut(&Secp256k1::verification_only()) // Just display the first error .map_err(|e| CommandError::FailedToPostOriginalPayjoinProposal(e[0].to_string()))?; + let original_txid = psbt.unsigned_tx.compute_txid(); - let persister = SenderPersister::new(Arc::new(self.db.clone())); + let persister = SenderPersister::new(Arc::new(self.db.clone()), &original_txid); log::info!("Saving new sender: {:?}", persister.session_id); let _sender = SenderBuilder::new(psbt.clone(), uri) .build_recommended(FeeRate::BROADCAST_MIN) @@ -484,19 +485,12 @@ impl DaemonControl { } } - for session_id in db_conn.get_all_active_sender_session_ids() { + if let Some(session_id) = db_conn.get_payjoin_session_id_from_txid(txid) { log::info!("Checking sender session: {:?}", session_id); let persister = SenderPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); - let (state, history) = replay_sender_event_log(&persister).unwrap(); + let (state, _) = replay_sender_event_log(&persister).unwrap(); log::info!("Sender state: {:?}", state); - let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); - if let Some(_original_txid) = original_txid { - // TODO: fix this - // if original_txid == *txid { - // TODO: this isnt a bip21, but a payjoin endpoint. Does this need to get returned? - return Ok(state.into()); - // } - } + return Ok(state.into()); } Ok(PayjoinStatus::Unknown) diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index d85d45741..48eadde10 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -220,7 +220,7 @@ pub trait DatabaseConnection { fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec>; /// Create a payjoin sender - fn save_new_payjoin_sender_session(&mut self) -> i64; + fn save_new_payjoin_sender_session(&mut self, original_txid: &bitcoin::Txid) -> i64; /// Get a all active payjoin senders fn get_all_active_sender_session_ids(&mut self) -> Vec; @@ -232,6 +232,12 @@ pub trait DatabaseConnection { /// Update the completed at timestamp for a sender session fn update_sender_session_completed_at(&mut self, session_id: &SessionId); + + /// Save the proposed txid for a sender session + fn save_proposed_payjoin_txid(&mut self, session_id: &SessionId, proposed_txid: &bitcoin::Txid); + + /// Get payjoin session id from txid -- this will return the session id if the txid is a proposed payjoin txid or the original txid + fn get_payjoin_session_id_from_txid(&mut self, txid: &bitcoin::Txid) -> Option; } impl DatabaseConnection for SqliteConn { @@ -483,8 +489,8 @@ impl DatabaseConnection for SqliteConn { self.load_receiver_session_events(session_id) } - fn save_new_payjoin_sender_session(&mut self) -> i64 { - self.save_new_payjoin_sender_session() + fn save_new_payjoin_sender_session(&mut self, original_txid: &bitcoin::Txid) -> i64 { + self.save_new_payjoin_sender_session(original_txid) } fn get_all_active_sender_session_ids(&mut self) -> Vec { @@ -502,6 +508,18 @@ impl DatabaseConnection for SqliteConn { fn update_sender_session_completed_at(&mut self, session_id: &SessionId) { self.update_sender_session_completed_at(session_id) } + + fn save_proposed_payjoin_txid( + &mut self, + session_id: &SessionId, + proposed_txid: &bitcoin::Txid, + ) { + self.save_proposed_payjoin_txid(session_id, proposed_txid) + } + + fn get_payjoin_session_id_from_txid(&mut self, txid: &bitcoin::Txid) -> Option { + self.get_payjoin_session_id_from_txid(txid) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 2cf396c72..56814f0b6 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -1085,12 +1085,12 @@ impl SqliteConn { .expect("Db must not fail") } - pub fn save_new_payjoin_sender_session(&mut self) -> i64 { + pub fn save_new_payjoin_sender_session(&mut self, original_txid: &bitcoin::Txid) -> i64 { let mut id = 0i64; db_exec(&mut self.conn, |db_tx| { db_tx.execute( - "INSERT INTO payjoin_senders (created_at) VALUES (?1)", - rusqlite::params![curr_timestamp()], + "INSERT INTO payjoin_senders (created_at, original_txid) VALUES (?1, ?2)", + rusqlite::params![curr_timestamp(), original_txid[..].to_vec()], )?; id = db_tx.last_insert_rowid(); Ok(()) @@ -1147,6 +1147,40 @@ impl SqliteConn { ) .expect("Db must not fail") } + + /// Save the proposed txid for a sender session + pub fn save_proposed_payjoin_txid( + &mut self, + session_id: &SessionId, + proposed_txid: &bitcoin::Txid, + ) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_senders SET proposed_txid = ?1 WHERE id = ?2", + rusqlite::params![proposed_txid[..].to_vec(), session_id.0], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + /// Get the payjoin session id from a txid + /// + /// This will return the session id if the txid is a proposed payjoin txid or the original txid + pub fn get_payjoin_session_id_from_txid(&mut self, txid: &bitcoin::Txid) -> Option { + // TODO: This should always be one row. + let session_id = db_query( + &mut self.conn, + "SELECT id FROM payjoin_senders WHERE proposed_txid = ?1 or original_txid = ?1", + rusqlite::params![txid[..].to_vec()], + |row| { + let id: i64 = row.get(0)?; + Ok(SessionId::new(id)) + }, + ) + .expect("Db must not fail"); + session_id.first().cloned() + } } #[cfg(test)] @@ -3825,10 +3859,14 @@ CREATE TABLE labels ( fn test_payjoin_sender_sessions() { let (temp_dir, _, _, db) = dummy_db(); let mut conn = db.connection().unwrap(); + let original_txid = bitcoin::Txid::from_str( + "0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(); - let session_id_1 = conn.save_new_payjoin_sender_session(); + let session_id_1 = conn.save_new_payjoin_sender_session(&original_txid); assert!(session_id_1 > 0); - let session_id_2 = conn.save_new_payjoin_sender_session(); + let session_id_2 = conn.save_new_payjoin_sender_session(&original_txid); assert!(session_id_2 > session_id_1); let active_sessions = conn.get_all_active_sender_session_ids(); @@ -3874,7 +3912,7 @@ CREATE TABLE labels ( assert_eq!(empty_events.len(), 0); // Test session with no events - let session_id_3 = conn.save_new_payjoin_sender_session(); + let session_id_3 = conn.save_new_payjoin_sender_session(&original_txid); let session_3 = SessionId::new(session_id_3); let no_events = conn.load_sender_session_events(&session_3); assert_eq!(no_events.len(), 0); diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 9aa96a58b..5e1573c94 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -135,6 +135,8 @@ CREATE TABLE payjoin_ohttp_keys ( CREATE TABLE payjoin_senders ( id INTEGER PRIMARY KEY NOT NULL, created_at INTEGER NOT NULL, + original_txid BLOB NOT NULL, + proposed_txid BLOB, completed_at INTEGER ); diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index f1a2097c5..be17d255f 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -1,3 +1,4 @@ +use payjoin::bitcoin; use payjoin::persist::SessionPersister; use payjoin::receive::v2::SessionEvent as ReceiverSessionEvent; use payjoin::send::v2::SessionEvent as SenderSessionEvent; @@ -98,9 +99,9 @@ pub struct SenderPersister { } impl SenderPersister { - pub fn new(db: Arc) -> Self { + pub fn new(db: Arc, original_txid: &bitcoin::Txid) -> Self { let mut db_conn = db.connection(); - let session_id = db_conn.save_new_payjoin_sender_session(); + let session_id = db_conn.save_new_payjoin_sender_session(original_txid); Self { db, session_id: SessionId(session_id), diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 2a359b8a4..4515082f9 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -137,7 +137,6 @@ pub fn payjoin_sender_check(db: &sync::Arc>) } } - // TODO: need a check here to see if this proposal already exists in the db let new_txid = proposal_psbt.unsigned_tx.compute_txid(); if db_conn.spend_tx(&new_txid).is_some() { log::info!("[Payjoin] Proposal already exists in the db"); @@ -149,6 +148,7 @@ pub fn payjoin_sender_check(db: &sync::Arc>) new_txid ); db_conn.store_spend(&proposal_psbt); + db_conn.save_proposed_payjoin_txid(&session_id, &new_txid); } Ok(None) => { log::info!("[Payjoin] Proposal not received yet..."); From 5848d9eeb8a3527f97a22c1067b2da2135f743fc Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 25 Jul 2025 13:23:27 -0400 Subject: [PATCH 50/72] Store original and proposed txids for pj receiver --- lianad/src/commands/mod.rs | 23 ++---------- lianad/src/database/mod.rs | 55 ++++++++++++++++++++++++++-- lianad/src/database/sqlite/mod.rs | 50 ++++++++++++++++++++++++- lianad/src/database/sqlite/schema.rs | 2 + lianad/src/payjoin/receiver.rs | 4 ++ 5 files changed, 111 insertions(+), 23 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 2d240e3b5..739f54460 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -463,29 +463,14 @@ impl DaemonControl { pub fn get_payjoin_info(&self, txid: &bitcoin::Txid) -> Result { let mut db_conn = self.db.connection(); info!("Getting payjoin info for txid: {:?}", txid); - for session_id in db_conn.get_all_active_receiver_session_ids() { + if let Some(session_id) = db_conn.get_payjoin_receiver_session_id_from_txid(txid) { let persister = ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); - let (state, history) = replay_receiver_event_log(&persister).unwrap(); - let original_txid = history.fallback_tx().map(|tx| tx.compute_txid()); - info!("Original txid {:?}", original_txid); - let ready_to_sign_txid = history - .psbt_ready_for_signing() - .map(|psbt| psbt.unsigned_tx.compute_txid()); - info!("Ready to sign txid {:?}", ready_to_sign_txid); - if let Some(ready_to_sign_txid) = ready_to_sign_txid { - if ready_to_sign_txid == *txid { - return Ok(state.into()); - } - } - if let Some(original_txid) = original_txid { - if original_txid == *txid { - return Ok(state.into()); - } - } + let (state, _) = replay_receiver_event_log(&persister).unwrap(); + return Ok(state.into()); } - if let Some(session_id) = db_conn.get_payjoin_session_id_from_txid(txid) { + if let Some(session_id) = db_conn.get_payjoin_sender_session_id_from_txid(txid) { log::info!("Checking sender session: {:?}", session_id); let persister = SenderPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); let (state, _) = replay_sender_event_log(&persister).unwrap(); diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 48eadde10..519b2ee72 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -206,6 +206,26 @@ pub trait DatabaseConnection { /// Save Receiver Session fn save_new_payjoin_receiver_session(&mut self) -> i64; + /// Save original txid for a receiver session + fn save_receiver_session_original_txid( + &mut self, + session_id: &SessionId, + original_txid: &bitcoin::Txid, + ); + + /// Save proposed txid for a receiver session + fn save_receiver_session_proposed_txid( + &mut self, + session_id: &SessionId, + proposed_txid: &bitcoin::Txid, + ); + + /// Get receiver session id from txid -- this will return the session id if the txid is a proposed payjoin txid or the original txid + fn get_payjoin_receiver_session_id_from_txid( + &mut self, + txid: &bitcoin::Txid, + ) -> Option; + /// Get all Receiver Sessions fn get_all_active_receiver_session_ids(&mut self) -> Vec; @@ -237,7 +257,10 @@ pub trait DatabaseConnection { fn save_proposed_payjoin_txid(&mut self, session_id: &SessionId, proposed_txid: &bitcoin::Txid); /// Get payjoin session id from txid -- this will return the session id if the txid is a proposed payjoin txid or the original txid - fn get_payjoin_session_id_from_txid(&mut self, txid: &bitcoin::Txid) -> Option; + fn get_payjoin_sender_session_id_from_txid( + &mut self, + txid: &bitcoin::Txid, + ) -> Option; } impl DatabaseConnection for SqliteConn { @@ -477,6 +500,29 @@ impl DatabaseConnection for SqliteConn { self.get_all_active_receiver_session_ids() } + fn save_receiver_session_original_txid( + &mut self, + session_id: &SessionId, + original_txid: &bitcoin::Txid, + ) { + self.update_receiver_session_original_txid(session_id, original_txid) + } + + fn save_receiver_session_proposed_txid( + &mut self, + session_id: &SessionId, + proposed_txid: &bitcoin::Txid, + ) { + self.update_receiver_session_proposed_txid(session_id, proposed_txid) + } + + fn get_payjoin_receiver_session_id_from_txid( + &mut self, + txid: &bitcoin::Txid, + ) -> Option { + self.get_payjoin_receiver_session_id(txid) + } + fn save_receiver_session_event(&mut self, session_id: &SessionId, event: Vec) { self.save_receiver_session_event(session_id, event) } @@ -517,8 +563,11 @@ impl DatabaseConnection for SqliteConn { self.save_proposed_payjoin_txid(session_id, proposed_txid) } - fn get_payjoin_session_id_from_txid(&mut self, txid: &bitcoin::Txid) -> Option { - self.get_payjoin_session_id_from_txid(txid) + fn get_payjoin_sender_session_id_from_txid( + &mut self, + txid: &bitcoin::Txid, + ) -> Option { + self.get_payjoin_sender_session_id(txid) } } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 56814f0b6..6c5f286de 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -1085,6 +1085,54 @@ impl SqliteConn { .expect("Db must not fail") } + /// Save original txid for a sender session + pub fn update_receiver_session_original_txid( + &mut self, + session_id: &SessionId, + original_txid: &bitcoin::Txid, + ) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_receivers SET original_txid = ?1 WHERE id = ?2", + rusqlite::params![original_txid[..].to_vec(), session_id.0], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + /// Save proposed txid for a sender session + pub fn update_receiver_session_proposed_txid( + &mut self, + session_id: &SessionId, + proposed_txid: &bitcoin::Txid, + ) { + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "UPDATE payjoin_receivers SET proposed_txid = ?1 WHERE id = ?2", + rusqlite::params![proposed_txid[..].to_vec(), session_id.0], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } + + /// Get receiver session id from txid -- this will return the session id if the txid is a proposed payjoin txid or the original txid + pub fn get_payjoin_receiver_session_id(&mut self, txid: &bitcoin::Txid) -> Option { + // TODO: This should always be one row. + let session_id = db_query( + &mut self.conn, + "SELECT id FROM payjoin_receivers WHERE proposed_txid = ?1 or original_txid = ?1", + rusqlite::params![txid[..].to_vec()], + |row| { + let id: i64 = row.get(0)?; + Ok(SessionId::new(id)) + }, + ) + .expect("Db must not fail"); + session_id.first().cloned() + } + pub fn save_new_payjoin_sender_session(&mut self, original_txid: &bitcoin::Txid) -> i64 { let mut id = 0i64; db_exec(&mut self.conn, |db_tx| { @@ -1167,7 +1215,7 @@ impl SqliteConn { /// Get the payjoin session id from a txid /// /// This will return the session id if the txid is a proposed payjoin txid or the original txid - pub fn get_payjoin_session_id_from_txid(&mut self, txid: &bitcoin::Txid) -> Option { + pub fn get_payjoin_sender_session_id(&mut self, txid: &bitcoin::Txid) -> Option { // TODO: This should always be one row. let session_id = db_query( &mut self.conn, diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 5e1573c94..eddf2f03b 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -152,6 +152,8 @@ CREATE TABLE payjoin_sender_events ( /* Payjoin receivers */ CREATE TABLE payjoin_receivers ( id INTEGER PRIMARY KEY NOT NULL, + original_txid BLOB, + proposed_txid BLOB, created_at INTEGER NOT NULL, completed_at INTEGER ); diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index e1303d686..76c64d504 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -237,6 +237,8 @@ fn apply_fee_range( .psbt_ready_for_signing() .expect("Just added fee applied psbt"); + let txid = psbt.unsigned_tx.compute_txid(); + db_conn.save_receiver_session_original_txid(&persister.session_id, &txid); db_conn.store_spend(&psbt); log::info!("[Payjoin] PSBT in the DB..."); @@ -276,6 +278,8 @@ fn finalize_proposal( }) .save(persister)?; + let proposed_txid = proposal.psbt().unsigned_tx.compute_txid(); + db_conn.save_receiver_session_proposed_txid(&persister.session_id, &proposed_txid); send_payjoin_proposal(proposal, persister)?; } } From a0dd289b6f07fa23bd3f2f2af66990b7a4fe6049 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Fri, 25 Jul 2025 13:33:07 -0400 Subject: [PATCH 51/72] Resolve missing `has_bip21` flag --- Cargo.lock | 6 +++--- liana-gui/src/app/view/receive.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e1ea82e8..0b0b2a12e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -592,7 +592,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.12.0", "serde", "unicode-normalization", ] @@ -4285,7 +4285,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.24.0" -source = "git+https://github.com/payjoin/rust-payjoin.git?branch=master#88b918f37a1af03ca3d48920ac8cd5ddcbe9dd44" +source = "git+https://github.com/payjoin/rust-payjoin.git?branch=master#5ecfed745bd33f6d5706f497bdb7662a3a7c9177" dependencies = [ "bhttp", "bitcoin", @@ -5323,7 +5323,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes 0.12.0", "rand 0.8.5", "secp256k1-sys", "serde", diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index 75d085089..ccbaa1ba1 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -113,7 +113,7 @@ fn address_card<'a>( .on_press(Message::Select(row_index)), ) .push(Space::with_width(Length::Fill)) - .push(if has_bip21 { + .push(if maybe_bip21.is_some() { button::secondary(None, "Show Bip21 QR Code") .on_press(Message::ShowBip21QrCode(row_index)) } else { From 24013d7c3aa989d4a9f8b859993d33e1dfe1af8a Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Mon, 28 Jul 2025 12:36:24 -0400 Subject: [PATCH 52/72] todo!() remaining test utils methods --- lianad/src/testutils.rs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 913d691d2..293a6b9c6 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -587,7 +587,7 @@ impl DatabaseConnection for DummyDatabase { .map(|id| SessionId(*id)) .collect() } - fn save_new_payjoin_sender_session(&mut self) -> i64 { + fn save_new_payjoin_sender_session(&mut self, _txid: &bitcoin::Txid) -> i64 { let id = self .db .read() @@ -690,6 +690,44 @@ impl DatabaseConnection for DummyDatabase { .or_insert(PayjoinSession { completed: false }) .completed = true; } + + fn save_receiver_session_original_txid( + &mut self, + _session_id: &SessionId, + _original_txid: &bitcoin::Txid, + ) { + todo!() + } + + fn save_receiver_session_proposed_txid( + &mut self, + _session_id: &SessionId, + _proposed_txid: &bitcoin::Txid, + ) { + todo!() + } + + fn get_payjoin_receiver_session_id_from_txid( + &mut self, + _txid: &bitcoin::Txid, + ) -> Option { + todo!() + } + + fn save_proposed_payjoin_txid( + &mut self, + _session_id: &SessionId, + _proposed_txid: &bitcoin::Txid, + ) { + todo!() + } + + fn get_payjoin_sender_session_id_from_txid( + &mut self, + _txid: &bitcoin::Txid, + ) -> Option { + todo!() + } } pub struct DummyLiana { From 61e1e4c74e268fd5aefbf8f4bdb93867c41c87d7 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Mon, 28 Jul 2025 12:37:31 -0400 Subject: [PATCH 53/72] Fix payjoin visibility in helpers.rs --- lianad/src/payjoin/helpers.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index efdfdf84a..c96a2e383 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -8,14 +8,14 @@ use miniscript::{ use payjoin::{bitcoin::Amount, IntoUrl, OhttpKeys}; use reqwest::{header::ACCEPT, Proxy}; -pub const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; +pub(crate) const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; -pub fn http_agent() -> reqwest::blocking::Client { +pub(crate) fn http_agent() -> reqwest::blocking::Client { reqwest::blocking::Client::new() } #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FetchOhttpKeysError { +pub(crate) enum FetchOhttpKeysError { Reqwest(String), InvalidOhttpKeys(String), InvalidUrl(String), @@ -30,7 +30,7 @@ impl std::fmt::Display for FetchOhttpKeysError { } } -pub fn fetch_ohttp_keys( +pub(crate) fn fetch_ohttp_keys( ohttp_relay: impl IntoUrl, payjoin_directory: impl IntoUrl, ) -> Result { @@ -75,7 +75,9 @@ fn validate_ohttp_keys_response( } } -pub fn post_request(req: payjoin::Request) -> Result { +pub(crate) fn post_request( + req: payjoin::Request, +) -> Result { let http = http_agent(); http.post(req.url) .header("Content-Type", req.content_type) @@ -84,7 +86,7 @@ pub fn post_request(req: payjoin::Request) -> Result) { +pub(crate) fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1) { let mut witness_utxo_to_clean = vec![]; let mut inputs_to_finalize = vec![]; for (index, input) in psbt.inputs.iter_mut().enumerate() { From c673fa69ccc9212c9f89091013a69fbc063a218f Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Mon, 28 Jul 2025 12:38:57 -0400 Subject: [PATCH 54/72] Consolidate use of OHTTP_RELAY const --- lianad/src/commands/mod.rs | 10 ++++------ lianad/src/payjoin/helpers.rs | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 739f54460..67f80778b 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -10,7 +10,7 @@ use crate::{ miniscript::bitcoin::absolute::LockTime, payjoin::{ db::{ReceiverPersister, SenderPersister}, - helpers::{fetch_ohttp_keys, FetchOhttpKeysError}, + helpers::{fetch_ohttp_keys, FetchOhttpKeysError, OHTTP_RELAY}, types::PayjoinStatus, }, poller::PollerMessage, @@ -385,18 +385,16 @@ impl DaemonControl { pub fn receive_payjoin(&self) -> Result { let mut db_conn = self.db.connection(); - // TODO(arturgontijo): Fetch these from DB (via GUI's Settings Panel) - let ohttp_relay: &str = "https://pj.bobspacebkk.com"; let directory = "https://payjo.in"; - let ohttp_keys = if let Some(entry) = db_conn.payjoin_get_ohttp_keys(ohttp_relay) { + let ohttp_keys = if let Some(entry) = db_conn.payjoin_get_ohttp_keys(OHTTP_RELAY) { entry.1 } else { - let ohttp_keys = std::thread::spawn(move || fetch_ohttp_keys(ohttp_relay, directory)) + let ohttp_keys = std::thread::spawn(move || fetch_ohttp_keys(OHTTP_RELAY, directory)) .join() .unwrap() .map_err(CommandError::FailedToFetchOhttpKeys)?; - db_conn.payjoin_save_ohttp_keys(ohttp_relay, ohttp_keys.clone()); + db_conn.payjoin_save_ohttp_keys(OHTTP_RELAY, ohttp_keys.clone()); ohttp_keys }; diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index c96a2e383..253bc513d 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -15,7 +15,7 @@ pub(crate) fn http_agent() -> reqwest::blocking::Client { } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum FetchOhttpKeysError { +pub enum FetchOhttpKeysError { Reqwest(String), InvalidOhttpKeys(String), InvalidUrl(String), From 056049ed9e9ec1c696eefde4ab4ade65f623af01 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Mon, 28 Jul 2025 12:40:26 -0400 Subject: [PATCH 55/72] Consolidate use of PAYJOIN_DIR const --- lianad/src/commands/mod.rs | 15 +++++++-------- lianad/src/payjoin/helpers.rs | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 67f80778b..f0a39b848 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -10,7 +10,7 @@ use crate::{ miniscript::bitcoin::absolute::LockTime, payjoin::{ db::{ReceiverPersister, SenderPersister}, - helpers::{fetch_ohttp_keys, FetchOhttpKeysError, OHTTP_RELAY}, + helpers::{fetch_ohttp_keys, FetchOhttpKeysError, OHTTP_RELAY, PAYJOIN_DIRECTORY}, types::PayjoinStatus, }, poller::PollerMessage, @@ -385,15 +385,14 @@ impl DaemonControl { pub fn receive_payjoin(&self) -> Result { let mut db_conn = self.db.connection(); - let directory = "https://payjo.in"; - let ohttp_keys = if let Some(entry) = db_conn.payjoin_get_ohttp_keys(OHTTP_RELAY) { entry.1 } else { - let ohttp_keys = std::thread::spawn(move || fetch_ohttp_keys(OHTTP_RELAY, directory)) - .join() - .unwrap() - .map_err(CommandError::FailedToFetchOhttpKeys)?; + let ohttp_keys = + std::thread::spawn(move || fetch_ohttp_keys(OHTTP_RELAY, PAYJOIN_DIRECTORY)) + .join() + .unwrap() + .map_err(CommandError::FailedToFetchOhttpKeys)?; db_conn.payjoin_save_ohttp_keys(OHTTP_RELAY, ohttp_keys.clone()); ohttp_keys }; @@ -413,7 +412,7 @@ impl DaemonControl { let persister = ReceiverPersister::new(Arc::new(self.db.clone())); let session = Receiver::::create_session( address.clone(), - directory, + PAYJOIN_DIRECTORY, ohttp_keys.clone(), None, ) diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 253bc513d..62a1ec1bf 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -9,6 +9,7 @@ use payjoin::{bitcoin::Amount, IntoUrl, OhttpKeys}; use reqwest::{header::ACCEPT, Proxy}; pub(crate) const OHTTP_RELAY: &str = "https://pj.bobspacebkk.com"; +pub(crate) const PAYJOIN_DIRECTORY: &str = "https://payjo.in"; pub(crate) fn http_agent() -> reqwest::blocking::Client { reqwest::blocking::Client::new() From 872b1fb14f7b418f8b5555efcb524ae6417d5550 Mon Sep 17 00:00:00 2001 From: user Date: Wed, 16 Jul 2025 13:18:15 -0400 Subject: [PATCH 56/72] Add insert_inputs_seen_before database method liana did not have a way to add coins from external wallets to the db, this adds those coins to the db and forces is_from_self to be false and sets the wallet_id to 2 which is never the users wallet as it is hardcoded to 1. --- lianad/src/database/mod.rs | 7 ++++++ lianad/src/database/sqlite/mod.rs | 30 +++++++++++++++++++++++++- lianad/src/database/sqlite/schema.rs | 32 ++++++++++++++++++++++++++++ lianad/src/payjoin/receiver.rs | 19 ++++++++++++----- 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index 519b2ee72..108f24cae 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -239,6 +239,9 @@ pub trait DatabaseConnection { /// Load all receiver session events for a particular session id fn load_receiver_session_events(&mut self, session_id: &SessionId) -> Vec>; + /// Check if input has been seen before and then add it to the input_seen table + fn insert_input_seen_before(&mut self, outpoints: &[bitcoin::OutPoint]) -> bool; + /// Create a payjoin sender fn save_new_payjoin_sender_session(&mut self, original_txid: &bitcoin::Txid) -> i64; /// Get a all active payjoin senders @@ -484,6 +487,10 @@ impl DatabaseConnection for SqliteConn { .collect() } + fn insert_input_seen_before(&mut self, outpoints: &[bitcoin::OutPoint]) -> bool { + self.insert_outpoint_seen_before(outpoints) + } + fn payjoin_get_ohttp_keys(&mut self, ohttp_relay: &str) -> Option<(u32, OhttpKeys)> { self.payjoin_get_ohttp_keys(ohttp_relay) } diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 6c5f286de..891620dc5 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -28,7 +28,7 @@ use crate::{ payjoin::db::SessionId, }; use liana::descriptors::LianaDescriptor; -use payjoin::OhttpKeys; +use payjoin::{bitcoin::consensus::Encodable, OhttpKeys}; use std::{ cmp, @@ -481,6 +481,34 @@ impl SqliteConn { .expect("Database must be available") } + pub fn insert_outpoint_seen_before<'a>( + &mut self, + outpoints: impl IntoIterator, + ) -> bool { + let mut is_duplicate = false; + db_exec(&mut self.conn, |db_tx| { + for outpoint in outpoints { + let mut buf = Vec::new(); + outpoint + .consensus_encode(&mut buf) + .expect("Outpoint must encode"); + let affected = db_tx.execute( + "INSERT OR IGNORE INTO payjoin_outpoints (outpoint, added_at) \ + VALUES (?1, ?2)", + rusqlite::params![buf, curr_timestamp()], + )?; + + if affected == 0 { + is_duplicate = true + } + } + Ok(()) + }) + .expect("database must be available"); + + is_duplicate + } + /// Remove a set of coins from the database. pub fn remove_coins(&mut self, outpoints: &[bitcoin::OutPoint]) { db_exec(&mut self.conn, |db_tx| { diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index eddf2f03b..d5f399164 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -1,5 +1,6 @@ use bip329::Label; use liana::descriptors::LianaDescriptor; +use payjoin::bitcoin::{consensus::Decodable, io::Cursor}; use std::{convert::TryFrom, str::FromStr}; @@ -87,6 +88,16 @@ CREATE TABLE coins ( ON DELETE RESTRICT ); +/* Seen Payjoin outpoints + * + * The 'added_at' field is simply the time that this outpoint is added to the table for + * tracking. + */ +CREATE TABLE payjoin_outpoints ( + outpoint BLOB NOT NULL PRIMARY KEY, + added_at INTEGER NOT NULL +); + /* A mapping from descriptor address to derivation index. Necessary until * we can get the derivation index from the parent descriptor from bitcoind. */ @@ -500,3 +511,24 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWalletTransaction { }) } } + +/// An outpoint we have seen before in payjoin transactions +#[derive(Clone, Debug, PartialEq)] +pub struct DbPayjoinOutpoint { + pub outpoint: bitcoin::OutPoint, + pub added_at: Option, +} + +impl TryFrom<&rusqlite::Row<'_>> for DbPayjoinOutpoint { + type Error = rusqlite::Error; + + fn try_from(row: &rusqlite::Row) -> Result { + let outpoint: Vec = row.get(0)?; + let outpoint = bitcoin::OutPoint::consensus_decode(&mut Cursor::new(outpoint)) + .expect("Outpoint should be decodable"); + + let added_at = row.get(1)?; + + Ok(DbPayjoinOutpoint { outpoint, added_at }) + } +} diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 76c64d504..3ce3f5dc4 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -20,6 +20,7 @@ use payjoin::{ }, InputPair, }, + ImplementationError, }; use crate::{ @@ -91,7 +92,10 @@ fn check_inputs_not_owned( ) -> Result<(), Box> { let proposal = proposal .check_inputs_not_owned(&mut |script| { - let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); + let address = + bitcoin::Address::from_script(script, db_conn.network()).map_err(|e| { + ImplementationError::from(Box::new(e) as Box) + })?; Ok(db_conn .derivation_index_by_address(&address) .map(|(index, is_change)| AddrInfo { index, is_change }) @@ -109,9 +113,10 @@ fn check_no_inputs_seen_before( secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { let proposal = proposal - // TODO implement check_no_inputs_seen_before callback and add new table to mark relevant - // outpoint as seen for the future - .check_no_inputs_seen_before(&mut |_| Ok(false)) + .check_no_inputs_seen_before(&mut |outpoint| { + let seen = db_conn.insert_input_seen_before(&[*outpoint]); + Ok(seen) + }) .save(persister)?; identify_receiver_outputs(proposal, persister, db_conn, desc, secp) } @@ -123,9 +128,13 @@ fn identify_receiver_outputs( desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { + log::debug!("[Payjoin] receiver outputs"); let proposal = proposal .identify_receiver_outputs(&mut |script| { - let address = bitcoin::Address::from_script(script, db_conn.network()).unwrap(); + let address = + bitcoin::Address::from_script(script, db_conn.network()).map_err(|e| { + ImplementationError::from(Box::new(e) as Box) + })?; Ok(db_conn .derivation_index_by_address(&address) .map(|(index, is_change)| AddrInfo { index, is_change }) From f1d726eb415f2aa48c569b6112f99efda0ebbd39 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 29 Jul 2025 10:50:11 -0400 Subject: [PATCH 57/72] Cleanup clippy warnings for CI We have a few stray clippy warnings causing our linter to fail in the CI. --- liana-gui/src/app/state/psbt.rs | 2 +- liana-gui/src/app/view/receive.rs | 2 +- lianad/src/payjoin/receiver.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index eedffdddd..ebea37c67 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -196,7 +196,7 @@ impl PsbtState { Message::View(view::Message::Spend(view::SpendTxMessage::PayjoinInitiated)) => { self.tx.status = SpendStatus::PayjoinInitiated; self.modal = None; - if let Some(_payjoin_info) = self.tx.payjoin_status.clone() { + if let Some(_payjoin_info) = self.tx.payjoin_status { let psbt = self.tx.psbt.clone(); // TODO: should this be an error? let bip21 = self.bip21.clone().expect("bip21 should be set"); diff --git a/liana-gui/src/app/view/receive.rs b/liana-gui/src/app/view/receive.rs index ccbaa1ba1..ab9662f06 100644 --- a/liana-gui/src/app/view/receive.rs +++ b/liana-gui/src/app/view/receive.rs @@ -84,7 +84,7 @@ fn address_card<'a>( Column::new() .push(Space::with_height(Length::Fixed(10.0))) .push( - p2_regular(&bip21.to_string()) + p2_regular(bip21.to_string()) .small() .style(theme::text::secondary), ) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 3ce3f5dc4..51b5146e2 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -261,7 +261,6 @@ fn finalize_proposal( db_conn: &mut Box, secp: &secp256k1::Secp256k1, ) -> Result<(), Box> { - let proposal = proposal; let (_, session_history) = replay_event_log(persister)?; let psbt = session_history .psbt_ready_for_signing() From d206772a53ad03ffc379bc0dca7f6bd9b971ba7b Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 29 Jul 2025 08:31:27 -0400 Subject: [PATCH 58/72] Clean up unused error fields in sendpayjoin modal --- liana-gui/src/app/state/psbt.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index ebea37c67..d63a6391e 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -188,7 +188,7 @@ impl PsbtState { self.modal = Some(PsbtModal::Delete(DeleteModal::default())); } Message::View(view::Message::Spend(view::SpendTxMessage::SendPayjoin)) => { - let modal = SendPayjoinModal::new(); + let modal = SendPayjoinModal; let cmd = modal.load(daemon); self.modal = Some(PsbtModal::SendPayjoin(modal)); return cmd; @@ -331,15 +331,7 @@ impl PsbtState { } #[derive(Default)] -pub struct SendPayjoinModal { - _error: Option, -} - -impl SendPayjoinModal { - pub fn new() -> Self { - Self { _error: None } - } -} +pub struct SendPayjoinModal; impl Modal for SendPayjoinModal { fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<'a, view::Message> { From fbd916eb82c144e5f0d88d3914da55bc22ac7425 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 29 Jul 2025 08:37:16 -0400 Subject: [PATCH 59/72] Remove unneeded debug logs --- liana-gui/src/app/state/spend/step.rs | 3 --- liana-gui/src/app/view/psbt.rs | 2 +- liana/src/signer.rs | 1 - lianad/src/commands/mod.rs | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/liana-gui/src/app/state/spend/step.rs b/liana-gui/src/app/state/spend/step.rs index ff37c9a19..b068e97b2 100644 --- a/liana-gui/src/app/state/spend/step.rs +++ b/liana-gui/src/app/state/spend/step.rs @@ -604,7 +604,6 @@ impl Step for DefineSpend { } view::CreateSpendMessage::Bip21Edited(i, bip21) => { - log::info!("Bip21Edited: {}", bip21); if let Some(recipient) = self.recipients.get_mut(i) { recipient.bip21.value = bip21.clone(); if let Ok(uri) = Uri::try_from(bip21.as_str()) { @@ -620,7 +619,6 @@ impl Step for DefineSpend { ); } if let Some(amount) = uri.amount { - log::info!("Amount: {}", amount); recipient.amount.value = amount.to_string_in(Denomination::Bitcoin); recipient.update( @@ -948,7 +946,6 @@ impl Recipient { self.label.value = label; } view::CreateSpendMessage::Bip21Edited(_, bip21) => { - log::info!("bip21: {}", bip21); self.bip21.value = bip21; } _ => {} diff --git a/liana-gui/src/app/view/psbt.rs b/liana-gui/src/app/view/psbt.rs index ebccab292..99f13787d 100644 --- a/liana-gui/src/app/view/psbt.rs +++ b/liana-gui/src/app/view/psbt.rs @@ -481,7 +481,7 @@ pub fn signatures<'a>( .push(p1_bold("Status")) .push(icon::circle_check_icon().style(theme::text::payjoin)) .push( - text("Payjoin Proposal Ready") + text("Payjoin Proposal Ready For Signing") .bold() .style(theme::text::payjoin), ), diff --git a/liana/src/signer.rs b/liana/src/signer.rs index fca8a19d5..2d88b9679 100644 --- a/liana/src/signer.rs +++ b/liana/src/signer.rs @@ -385,7 +385,6 @@ impl HotSigner { &mut psbt.inputs[i], i, )?; - log::info!("Signed input at {}", i); } else { self.sign_taproot( secp, diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index f0a39b848..4ff43be66 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -906,7 +906,6 @@ impl DaemonControl { // effort basis. let txid = tx.compute_txid(); if let Some(mut db_psbt) = db_conn.spend_tx(&txid) { - info!("Updating spend: {:?}", txid); let db_tx = db_psbt.unsigned_tx.clone(); for i in 0..db_tx.input.len() { if tx From 58757e1b382a83fc59c3ad8c22de52f78e7b04e1 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 29 Jul 2025 08:40:33 -0400 Subject: [PATCH 60/72] Fix visibility in payjoin sub mod --- lianad/src/payjoin/db.rs | 6 +++--- lianad/src/payjoin/mod.rs | 8 ++++---- lianad/src/payjoin/receiver.rs | 2 +- lianad/src/payjoin/sender.rs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index be17d255f..78987699b 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -18,7 +18,7 @@ impl SessionId { } #[derive(Debug)] -pub enum PersisterError { +pub(crate) enum PersisterError { Serialize(serde_json::Error), Deserialize(serde_json::Error), NotFound(String), @@ -37,7 +37,7 @@ impl Display for PersisterError { impl std::error::Error for PersisterError {} #[derive(Clone)] -pub struct ReceiverPersister { +pub(crate) struct ReceiverPersister { db: Arc, pub session_id: SessionId, } @@ -93,7 +93,7 @@ impl SessionPersister for ReceiverPersister { } #[derive(Clone)] -pub struct SenderPersister { +pub(crate) struct SenderPersister { db: Arc, pub session_id: SessionId, } diff --git a/lianad/src/payjoin/mod.rs b/lianad/src/payjoin/mod.rs index 0761f8397..ec93c8c97 100644 --- a/lianad/src/payjoin/mod.rs +++ b/lianad/src/payjoin/mod.rs @@ -1,5 +1,5 @@ -pub mod db; -pub mod helpers; -pub mod receiver; -pub mod sender; +pub(crate) mod db; +pub(crate) mod helpers; +pub(crate) mod receiver; +pub(crate) mod sender; pub mod types; diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 51b5146e2..328320c97 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -362,7 +362,7 @@ fn process_receiver_session( Ok(()) } -pub fn payjoin_receiver_check( +pub(crate) fn payjoin_receiver_check( db: &sync::Arc>, bit: &mut sync::Arc>, desc: &descriptors::LianaDescriptor, diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 4515082f9..5b43482b6 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -89,7 +89,7 @@ fn process_sender_session( } } -pub fn payjoin_sender_check(db: &sync::Arc>) { +pub(crate) fn payjoin_sender_check(db: &sync::Arc>) { let mut db_conn = db.connection(); for session_id in db_conn.get_all_active_sender_session_ids() { let persister = SenderPersister::from_id(Arc::new(db.clone()), session_id.clone()); From 27663ad9b1bab0895ba851cd9eedd50a77800074 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 29 Jul 2025 09:42:04 -0400 Subject: [PATCH 61/72] Remove unused db error variant --- lianad/src/payjoin/db.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 78987699b..72552ae7b 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -21,7 +21,6 @@ impl SessionId { pub(crate) enum PersisterError { Serialize(serde_json::Error), Deserialize(serde_json::Error), - NotFound(String), } impl Display for PersisterError { @@ -29,7 +28,6 @@ impl Display for PersisterError { match self { PersisterError::Serialize(e) => write!(f, "Serialization failed: {e}"), PersisterError::Deserialize(e) => write!(f, "Deserialization failed: {e}"), - PersisterError::NotFound(key) => write!(f, "Key not found: {key}"), } } } From 690bfffdf45b739637b5542dd37a93b4668a73f8 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 29 Jul 2025 09:49:02 -0400 Subject: [PATCH 62/72] Lazily deserialize session events in load() --- lianad/src/payjoin/db.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 72552ae7b..75e524ae8 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -64,7 +64,6 @@ impl SessionPersister for ReceiverPersister { event: &Self::SessionEvent, ) -> std::result::Result<(), Self::InternalStorageError> { let mut db_conn = self.db.connection(); - // serilize event let event_ser = serde_json::to_vec(event).map_err(PersisterError::Serialize)?; db_conn.save_receiver_session_event(&self.session_id, event_ser); Ok(()) @@ -76,11 +75,10 @@ impl SessionPersister for ReceiverPersister { { let mut db_conn = self.db.connection(); let events = db_conn.load_receiver_session_events(&self.session_id); - let deserialized_events: Result, _> = events + let iter = events .into_iter() - .map(|event| serde_json::from_slice(&event).map_err(PersisterError::Deserialize)) - .collect(); - Ok(Box::new(deserialized_events?.into_iter())) + .map(|event| serde_json::from_slice(&event).expect("Event to be serialized correctly")); + Ok(Box::new(iter)) } fn close(&self) -> std::result::Result<(), Self::InternalStorageError> { @@ -120,7 +118,6 @@ impl SessionPersister for SenderPersister { event: &Self::SessionEvent, ) -> std::result::Result<(), Self::InternalStorageError> { let mut db_conn = self.db.connection(); - // serilize event let event_ser = serde_json::to_vec(event).map_err(PersisterError::Serialize)?; db_conn.save_sender_session_event(&self.session_id, event_ser); Ok(()) @@ -132,11 +129,10 @@ impl SessionPersister for SenderPersister { { let mut db_conn = self.db.connection(); let events = db_conn.get_all_sender_session_events(&self.session_id); - let deserialized_events: Result, _> = events + let iter = events .into_iter() - .map(|event| serde_json::from_slice(&event).map_err(PersisterError::Deserialize)) - .collect(); - Ok(Box::new(deserialized_events?.into_iter())) + .map(|event| serde_json::from_slice(&event).expect("Event to be serialized correctly")); + Ok(Box::new(iter)) } fn close(&self) -> std::result::Result<(), Self::InternalStorageError> { From 69a6dec3d115f37a425d5604e12a3f11cb48da94 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Tue, 29 Jul 2025 09:58:06 -0400 Subject: [PATCH 63/72] Add rustdocs to finalize_psbt() --- lianad/src/payjoin/helpers.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lianad/src/payjoin/helpers.rs b/lianad/src/payjoin/helpers.rs index 62a1ec1bf..dd31c76f2 100644 --- a/lianad/src/payjoin/helpers.rs +++ b/lianad/src/payjoin/helpers.rs @@ -87,6 +87,8 @@ pub(crate) fn post_request( .send() } +/// Optimistically attempt to create witness for all inputs. +/// This method will not fail even if some inputs are not finalized or include invalid partial signatures. pub(crate) fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1) { let mut witness_utxo_to_clean = vec![]; let mut inputs_to_finalize = vec![]; @@ -117,8 +119,8 @@ pub(crate) fn finalize_psbt(psbt: &mut Psbt, secp: &secp256k1::Secp256k1 log::info!("[Payjoin] Finalizing input at: {}", index), - Err(e) => log::warn!("[Payjoin] Failed to finalize input at: {} | {}", index, e), + Ok(_) => log::info!("Finalizing input at: {}", index), + Err(e) => log::warn!("Failed to finalize input at: {} | {}", index, e), } } From 69b6743311edeef97e406ed37d7c237591660ba0 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 30 Jul 2025 09:51:02 -0400 Subject: [PATCH 64/72] Restore psbt input fields after finalizing psbt --- lianad/src/commands/mod.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 4ff43be66..30be76553 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -440,15 +440,21 @@ impl DaemonControl { .map_err(|_| "URI does not support Payjoin".to_string()) .unwrap(); - let mut psbt = psbt.clone(); - psbt.finalize_mut(&Secp256k1::verification_only()) + let mut signed_psbt = psbt.clone(); + signed_psbt + .finalize_mut(&Secp256k1::verification_only()) // Just display the first error .map_err(|e| CommandError::FailedToPostOriginalPayjoinProposal(e[0].to_string()))?; - let original_txid = psbt.unsigned_tx.compute_txid(); + let mut original_psbt = psbt.clone(); + for (index, input) in original_psbt.inputs.iter_mut().enumerate() { + input.partial_sigs = Default::default(); + input.final_script_witness = signed_psbt.inputs[index].final_script_witness.clone(); + } + + let original_txid = original_psbt.unsigned_tx.compute_txid(); let persister = SenderPersister::new(Arc::new(self.db.clone()), &original_txid); - log::info!("Saving new sender: {:?}", persister.session_id); - let _sender = SenderBuilder::new(psbt.clone(), uri) + let _sender = SenderBuilder::new(original_psbt.clone(), uri) .build_recommended(FeeRate::BROADCAST_MIN) .save(&persister) .unwrap(); From eec4ea3d0e9feca2bf0012f6658662d7e6b7bdff Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 30 Jul 2025 12:05:02 -0400 Subject: [PATCH 65/72] Let rust-payjoin restore psbt inputs --- Cargo.lock | 2 +- lianad/src/payjoin/sender.rs | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b0b2a12e..2eda07e4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4285,7 +4285,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "payjoin" version = "0.24.0" -source = "git+https://github.com/payjoin/rust-payjoin.git?branch=master#5ecfed745bd33f6d5706f497bdb7662a3a7c9177" +source = "git+https://github.com/payjoin/rust-payjoin.git?branch=master#ce0c7716be1d5e24168960ee824dc1b69359f943" dependencies = [ "bhttp", "bitcoin", diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index 5b43482b6..aaacd3647 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -2,7 +2,6 @@ use crate::database::DatabaseInterface; use crate::payjoin::helpers::post_request; -use std::collections::HashMap; use std::error::Error; use std::sync::{self, Arc}; @@ -116,27 +115,10 @@ pub(crate) fn payjoin_sender_check(db: &sync::Arc { - let mut proposal_psbt = proposal_psbt; - // TODO(arturgontijo): PDK removes fields that we need in the GUI to properly sign the inputs - let mut input_fields_to_restore = HashMap::new(); - for (index, txin) in original_psbt.unsigned_tx.input.iter().enumerate() { - let mut input_without_sigs = original_psbt.inputs[index].clone(); - input_without_sigs.partial_sigs = Default::default(); - input_fields_to_restore.insert(txin.previous_output, input_without_sigs); - } let original_txid = original_psbt.unsigned_tx.compute_txid(); // TODO: should we be deleting the original psbt? can we fallback without it? log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); db_conn.delete_spend(&original_txid); - - // Restoring witness_scripts and bip32_derivation so GUI can sign them - for (index, psbtin) in proposal_psbt.inputs.iter_mut().enumerate() { - let outpoint = &proposal_psbt.unsigned_tx.input[index].previous_output; - if let Some(input) = input_fields_to_restore.get(outpoint) { - *psbtin = input.clone(); - } - } - let new_txid = proposal_psbt.unsigned_tx.compute_txid(); if db_conn.spend_tx(&new_txid).is_some() { log::info!("[Payjoin] Proposal already exists in the db"); From 39ee2e75cd263dd8d8a339a12a1832ffb6b294e4 Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Wed, 30 Jul 2025 12:05:38 -0400 Subject: [PATCH 66/72] Remove unused db deser error variant --- lianad/src/payjoin/db.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/lianad/src/payjoin/db.rs b/lianad/src/payjoin/db.rs index 75e524ae8..bf4883042 100644 --- a/lianad/src/payjoin/db.rs +++ b/lianad/src/payjoin/db.rs @@ -20,14 +20,12 @@ impl SessionId { #[derive(Debug)] pub(crate) enum PersisterError { Serialize(serde_json::Error), - Deserialize(serde_json::Error), } impl Display for PersisterError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { PersisterError::Serialize(e) => write!(f, "Serialization failed: {e}"), - PersisterError::Deserialize(e) => write!(f, "Deserialization failed: {e}"), } } } From 6f74743defd12a64d6dd71c5c20d3c01e823a65c Mon Sep 17 00:00:00 2001 From: Armin Sabouri Date: Thu, 14 Aug 2025 11:56:28 -0400 Subject: [PATCH 67/72] Remove buggy sender fallback tx check --- lianad/src/payjoin/sender.rs | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index aaacd3647..b8f720e1e 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -97,25 +97,13 @@ pub(crate) fn payjoin_sender_check(db: &sync::Arc { - // Get the original psbt so we can restore the input fields - let original_psbt = db_conn.spend_tx(&txid); - if original_psbt.is_none() { - log::error!("[Payjoin] expecting fallback txid for session={session_id:?}, but none found"); - return; - } - original_psbt.expect("checked above") - } - None => { - log::info!("[Payjoin] No fallback txid found for session={session_id:?}"); - return; - } - }; match process_sender_session(state, &persister) { Ok(Some(proposal_psbt)) => { - let original_txid = original_psbt.unsigned_tx.compute_txid(); + let original_txid = session_history + .fallback_tx() + .map(|tx| tx.compute_txid()) + .expect("fallback tx should be present"); // TODO: should we be deleting the original psbt? can we fallback without it? log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); db_conn.delete_spend(&original_txid); From 2534935bded6ab617d8045d72fef14b7498ecdad Mon Sep 17 00:00:00 2001 From: user Date: Wed, 30 Jul 2025 14:14:59 -0400 Subject: [PATCH 68/72] Add todo insert_inputs_seen dummmy test utils method This missing method in the dummy test utils was again causing some more clippy faliures. --- lianad/src/testutils.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index 293a6b9c6..3f686cd7c 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -728,6 +728,10 @@ impl DatabaseConnection for DummyDatabase { ) -> Option { todo!() } + + fn insert_input_seen_before(&mut self, _outpoints: &[bitcoin::OutPoint]) -> bool { + todo!() + } } pub struct DummyLiana { From 7b5896ba66f49f246d0a576ae4a6e19d51309fb9 Mon Sep 17 00:00:00 2001 From: user Date: Thu, 14 Aug 2025 13:51:37 -0400 Subject: [PATCH 69/72] Process Receiver sessions is no longer halted by a failed session The for loop in our receiver session was causing any error in a session to bubble up and completely stop the loop meaning follow-up sessions could not proceed forward. --- lianad/src/payjoin/receiver.rs | 79 +++++++++++++++++----------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index 328320c97..ec2bb3f1e 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -316,48 +316,42 @@ fn send_payjoin_proposal( } fn process_receiver_session( - db: &sync::Arc>, + db_conn: &mut Box, bit: &mut sync::Arc>, desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, + persister: ReceiverPersister, ) -> Result<(), Box> { - let mut db_conn = db.connection(); - for session_id in db_conn.get_all_active_receiver_session_ids() { - let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()); - - let (state, _) = replay_event_log(&persister) - .map_err(|e| format!("Failed to replay receiver event log: {:?}", e))?; + let (state, _) = replay_event_log(&persister) + .map_err(|e| format!("Failed to replay receiver event log: {:?}", e))?; - match state { - ReceiveSession::Initialized(context) => { - read_from_directory(context, &persister, &mut db_conn, bit, desc, secp)?; - } - ReceiveSession::UncheckedProposal(proposal) => { - check_proposal(proposal, &persister, &mut db_conn, bit, desc, secp)?; - } - ReceiveSession::MaybeInputsOwned(proposal) => { - check_inputs_not_owned(proposal, &persister, &mut db_conn, desc, secp)?; - } - ReceiveSession::MaybeInputsSeen(proposal) => { - check_no_inputs_seen_before(proposal, &persister, &mut db_conn, desc, secp)?; - } - ReceiveSession::OutputsUnknown(proposal) => { - identify_receiver_outputs(proposal, &persister, &mut db_conn, desc, secp)?; - } - ReceiveSession::WantsOutputs(proposal) => { - commit_outputs(proposal, &persister, &mut db_conn, desc, secp)?; - } - ReceiveSession::WantsInputs(proposal) => { - contribute_inputs(proposal, &persister, &mut db_conn, desc, secp)? - } - ReceiveSession::ProvisionalProposal(proposal) => { - finalize_proposal(proposal, &persister, &mut db_conn, secp)? - } - ReceiveSession::PayjoinProposal(proposal) => { - send_payjoin_proposal(proposal, &persister)? - } - _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), + match state { + ReceiveSession::Initialized(context) => { + read_from_directory(context, &persister, db_conn, bit, desc, secp)?; + } + ReceiveSession::UncheckedProposal(proposal) => { + check_proposal(proposal, &persister, db_conn, bit, desc, secp)?; + } + ReceiveSession::MaybeInputsOwned(proposal) => { + check_inputs_not_owned(proposal, &persister, db_conn, desc, secp)?; + } + ReceiveSession::MaybeInputsSeen(proposal) => { + check_no_inputs_seen_before(proposal, &persister, db_conn, desc, secp)?; + } + ReceiveSession::OutputsUnknown(proposal) => { + identify_receiver_outputs(proposal, &persister, db_conn, desc, secp)?; + } + ReceiveSession::WantsOutputs(proposal) => { + commit_outputs(proposal, &persister, db_conn, desc, secp)?; + } + ReceiveSession::WantsInputs(proposal) => { + contribute_inputs(proposal, &persister, db_conn, desc, secp)? } + ReceiveSession::ProvisionalProposal(proposal) => { + finalize_proposal(proposal, &persister, db_conn, secp)? + } + ReceiveSession::PayjoinProposal(proposal) => send_payjoin_proposal(proposal, &persister)?, + _ => return Err(format!("Unexpected receiver state: {:?}", state).into()), } Ok(()) } @@ -368,8 +362,15 @@ pub(crate) fn payjoin_receiver_check( desc: &descriptors::LianaDescriptor, secp: &secp256k1::Secp256k1, ) { - match process_receiver_session(db, bit, desc, secp) { - Ok(_) => (), - Err(e) => log::warn!("process_receiver_session(): {}", e), + let mut db_conn = db.connection(); + for session_id in db_conn.get_all_active_receiver_session_ids() { + let persister = ReceiverPersister::from_id(Arc::new(db.clone()), session_id.clone()); + match process_receiver_session(&mut db_conn, bit, desc, secp, persister) { + Ok(_) => (), + Err(e) => { + log::warn!("process_receiver_session(): {}", e); + continue; + } + } } } From d7f1bff3aa815ca55d38aa9a04242d1f91c2259c Mon Sep 17 00:00:00 2001 From: user Date: Thu, 14 Aug 2025 14:44:17 -0400 Subject: [PATCH 70/72] Sender checks skip failed sessions --- lianad/src/payjoin/sender.rs | 93 ++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/lianad/src/payjoin/sender.rs b/lianad/src/payjoin/sender.rs index b8f720e1e..15218fc2f 100644 --- a/lianad/src/payjoin/sender.rs +++ b/lianad/src/payjoin/sender.rs @@ -1,5 +1,6 @@ -use crate::database::DatabaseInterface; +use crate::database::{DatabaseConnection, DatabaseInterface}; +use crate::payjoin::db::SessionId; use crate::payjoin::helpers::post_request; use std::error::Error; @@ -7,7 +8,7 @@ use std::sync::{self, Arc}; use payjoin::bitcoin::Psbt; use payjoin::persist::OptionalTransitionOutcome; -use payjoin::send::v2::{replay_event_log, SendSession, V2GetContext}; +use payjoin::send::v2::{replay_event_log, SendSession, SessionHistory, V2GetContext}; use payjoin::send::v2::{Sender, WithReplyKey}; use super::db::SenderPersister; @@ -43,6 +44,35 @@ fn get_proposed_payjoin_psbt( } } +fn update_db_with_psbt( + db_conn: &mut Box, + session_history: &SessionHistory, + session_id: &SessionId, + psbt: Psbt, +) { + let original_txid = session_history + .fallback_tx() + .map(|tx| tx.compute_txid()) + .expect("fallback tx should be present"); + + log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); + db_conn.delete_spend(&original_txid); + + let new_txid = psbt.unsigned_tx.compute_txid(); + if db_conn.spend_tx(&new_txid).is_some() { + log::info!("[Payjoin] Proposal already exists in the db"); + return; + } + + log::info!( + "[Payjoin] Updating Payjoin psbt: {} -> {}", + original_txid, + new_txid + ); + db_conn.store_spend(&psbt); + db_conn.save_proposed_payjoin_txid(session_id, &new_txid); +} + fn post_orginal_proposal( sender: Sender, persister: &SenderPersister, @@ -61,28 +91,35 @@ fn post_orginal_proposal( } fn process_sender_session( - state: SendSession, + db_conn: &mut Box, + session_id: SessionId, persister: &SenderPersister, -) -> Result, Box> { +) -> Result<(), Box> { + let (state, session_history) = replay_event_log(persister) + .map_err(|e| format!("Failed to replay sender event log: {:?}", e))?; + match state { SendSession::WithReplyKey(sender) => { log::info!("[Payjoin] SenderState::WithReplyKey"); - match post_orginal_proposal(sender, persister) { - Ok(_) => {} - Err(err) => log::warn!("post_orginal_proposal(): {}", err), + if let Err(err) = post_orginal_proposal(sender, persister) { + log::warn!("post_orginal_proposal(): {}", err); } - Ok(None) + Ok(()) } SendSession::V2GetContext(context) => { log::info!("[Payjoin] SenderState::V2GetContext"); - get_proposed_payjoin_psbt(context, persister) + if let Ok(Some(psbt)) = get_proposed_payjoin_psbt(context, persister) { + update_db_with_psbt(db_conn, &session_history, &session_id, psbt); + } + Ok(()) } SendSession::ProposalReceived(psbt) => { log::info!( "[Payjoin] SenderState::ProposalReceived: {}", psbt.to_string() ); - Ok(Some(psbt.clone())) + update_db_with_psbt(db_conn, &session_history, &session_id, psbt.clone()); + Ok(()) } _ => Err("Unexpected sender state".into()), } @@ -92,38 +129,12 @@ pub(crate) fn payjoin_sender_check(db: &sync::Arc { - let original_txid = session_history - .fallback_tx() - .map(|tx| tx.compute_txid()) - .expect("fallback tx should be present"); - // TODO: should we be deleting the original psbt? can we fallback without it? - log::info!("[Payjoin] Deleting original Payjoin psbt (txid={original_txid})"); - db_conn.delete_spend(&original_txid); - let new_txid = proposal_psbt.unsigned_tx.compute_txid(); - if db_conn.spend_tx(&new_txid).is_some() { - log::info!("[Payjoin] Proposal already exists in the db"); - return; - } - log::info!( - "[Payjoin] Updating Payjoin psbt: {} -> {}", - original_txid, - new_txid - ); - db_conn.store_spend(&proposal_psbt); - db_conn.save_proposed_payjoin_txid(&session_id, &new_txid); - } - Ok(None) => { - log::info!("[Payjoin] Proposal not received yet..."); + match process_sender_session(&mut db_conn, session_id, &persister) { + Ok(_) => (), + Err(e) => { + log::warn!("payjoin_sender_check(): {}", e); + continue; } - Err(e) => log::warn!("payjoin_sender_check(): {}", e), } } } From e62a806b9709882164ff5fa9d6e663bd3949aa60 Mon Sep 17 00:00:00 2001 From: user Date: Mon, 18 Aug 2025 12:03:26 -0400 Subject: [PATCH 71/72] Prevent expired sessions from panicing --- lianad/src/payjoin/receiver.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lianad/src/payjoin/receiver.rs b/lianad/src/payjoin/receiver.rs index ec2bb3f1e..d304a487b 100644 --- a/lianad/src/payjoin/receiver.rs +++ b/lianad/src/payjoin/receiver.rs @@ -42,7 +42,8 @@ fn read_from_directory( let mut receiver = receiver; let (req, context) = receiver .create_poll_request(OHTTP_RELAY) - .expect("Failed to extract request"); + .map_err(|e| format!("Failed to extract request: {:?}", e))?; + let proposal = match post_request(req.clone()) { Ok(ohttp_response) => { let response_bytes = ohttp_response.bytes()?; From b62fe490262164fe03beeadc8b82964addd1016b Mon Sep 17 00:00:00 2001 From: user Date: Mon, 18 Aug 2025 13:56:44 -0400 Subject: [PATCH 72/72] handle replay session events errors --- lianad/src/commands/mod.rs | 10 ++++++++-- lianad/src/jsonrpc/rpc.rs | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 30be76553..5d7af1ba7 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -90,6 +90,7 @@ pub enum CommandError { FailedToFetchOhttpKeys(FetchOhttpKeysError), // Same FIXME as `SpendFinalization` FailedToPostOriginalPayjoinProposal(String), + ReplayError(String), } impl fmt::Display for CommandError { @@ -152,6 +153,9 @@ impl fmt::Display for CommandError { Self::FailedToPostOriginalPayjoinProposal(e) => { write!(f, "Failed to post original payjoin proposal: '{}'.", e) } + Self::ReplayError(e) => { + write!(f, "Payjoin replay failed: '{}'.", e) + } } } } @@ -469,14 +473,16 @@ impl DaemonControl { if let Some(session_id) = db_conn.get_payjoin_receiver_session_id_from_txid(txid) { let persister = ReceiverPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); - let (state, _) = replay_receiver_event_log(&persister).unwrap(); + let (state, _) = replay_receiver_event_log(&persister) + .map_err(|e| CommandError::ReplayError(format!("Receiver replay failed: {e:?}")))?; return Ok(state.into()); } if let Some(session_id) = db_conn.get_payjoin_sender_session_id_from_txid(txid) { log::info!("Checking sender session: {:?}", session_id); let persister = SenderPersister::from_id(Arc::new(self.db.clone()), session_id.clone()); - let (state, _) = replay_sender_event_log(&persister).unwrap(); + let (state, _) = replay_sender_event_log(&persister) + .map_err(|e| CommandError::ReplayError(format!("Sender replay failed: {e:?}")))?; log::info!("Sender state: {:?}", state); return Ok(state.into()); } diff --git a/lianad/src/jsonrpc/rpc.rs b/lianad/src/jsonrpc/rpc.rs index de56ed6ec..67b9e94b6 100644 --- a/lianad/src/jsonrpc/rpc.rs +++ b/lianad/src/jsonrpc/rpc.rs @@ -50,6 +50,7 @@ pub struct Request { /// A failure to broadcast a transaction to the P2P network. const BROADCAST_ERROR: i64 = 1_000; +const REPLAY_ERROR: i64 = 1_001; /// JSONRPC2 error codes. See https://www.jsonrpc.org/specification#error_object. #[derive(Debug, PartialEq, Eq, Clone)] @@ -177,6 +178,9 @@ impl From for Error { commands::CommandError::FailedToPostOriginalPayjoinProposal(_) => { Error::new(ErrorCode::ServerError(BROADCAST_ERROR), e.to_string()) } + commands::CommandError::ReplayError(_) => { + Error::new(ErrorCode::ServerError(REPLAY_ERROR), e.to_string()) + } } } }