diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ab6e9dbc9..8873597f8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,6 +53,41 @@ jobs: cd ../cli cargo clippy -- -D warnings + build: + name: Cargo Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + lib -> target + cache-on-failure: "true" + + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + version: "27.2" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - run: cargo build + working-directory: lib + + - name: Check git status + env: + GIT_PAGER: cat + run: | + status=$(git status --porcelain) + if [[ -n "$status" ]]; then + echo "Git status has changes" + echo "$status" + git diff + exit 1 + else + echo "No changes in git status" + fi + tests: name: Test sdk-core runs-on: ubuntu-latest @@ -341,4 +376,4 @@ jobs: rm Cargo.lock cargo update --package secp256k1-zkp - cargo clippy -- -D warnings \ No newline at end of file + cargo clippy -- -D warnings diff --git a/cli/Cargo.lock b/cli/Cargo.lock index e44414c5a..f27bc6475 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -4,19 +4,13 @@ version = 4 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -93,9 +87,9 @@ dependencies = [ [[package]] name = "allo-isolate" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b6d794345b06592d0ebeed8e477e41b71e5a0a49df4fc0e4184d5938b99509" +checksum = "1f67642eb6773fb42a95dd3b348c305ee18dee6642274c6b412d67e985e3befc" dependencies = [ "anyhow", "atomic", @@ -138,9 +132,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -153,47 +147,53 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" dependencies = [ "backtrace", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -212,7 +212,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -224,7 +224,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", "synstructure", ] @@ -236,14 +236,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -252,24 +252,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -297,9 +297,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" @@ -308,13 +308,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "itoa", "matchit", "memchr", @@ -324,7 +324,34 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.2", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -346,19 +373,39 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -419,11 +466,11 @@ dependencies = [ [[package]] name = "bip39" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" +checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" dependencies = [ - "bitcoin_hashes 0.11.0", + "bitcoin_hashes 0.13.0", "rand", "rand_core", "serde", @@ -472,9 +519,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "bech32 0.11.0", @@ -652,33 +699,39 @@ dependencies = [ "bip39", "boltz-client", "chrono", + "ecies", "electrum-client", "env_logger 0.11.5", "flutter_rust_bridge", "futures-util", "glob", "hex", + "lazy_static", "lightning 0.0.125", "log", "lwk_common", "lwk_signer", "lwk_wollet", "openssl", + "prost 0.13.4", "reqwest 0.11.20", "rusqlite", "rusqlite_migration", "sdk-common", "security-framework", "security-framework-sys", + "semver", "serde", "serde_json", "strum", "strum_macros", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "tokio-stream", "tokio-tungstenite", + "tonic 0.12.3", + "tonic-build 0.12.3", "url", "x509-parser", "zbase32", @@ -698,9 +751,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] name = "byteorder" @@ -710,9 +763,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbc" @@ -725,9 +778,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.13" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] @@ -738,11 +791,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -774,9 +833,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.16" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -784,9 +843,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.15" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -796,21 +855,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" @@ -823,9 +882,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "console_error_panic_hook" @@ -855,9 +914,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -871,6 +930,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -958,7 +1029,7 @@ checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -984,6 +1055,15 @@ dependencies = [ "powerfmt", ] +[[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" @@ -1003,7 +1083,24 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", +] + +[[package]] +name = "ecies" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0206e602d2645ec8b24ed8307fadbc6c3110e2b11ab2f806fc02fee49327079" +dependencies = [ + "getrandom", + "hkdf", + "libsecp256k1", + "once_cell", + "openssl", + "parking_lot 0.12.3", + "rand_core", + "sha2", + "wasm-bindgen", ] [[package]] @@ -1055,9 +1152,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -1122,19 +1219,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "error-code" -version = "3.2.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "fallible-iterator" @@ -1150,9 +1247,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" @@ -1173,12 +1270,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -1220,7 +1317,7 @@ dependencies = [ "md-5", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -1255,9 +1352,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1270,9 +1367,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1280,15 +1377,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1297,38 +1394,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1363,9 +1460,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -1376,9 +1473,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -1398,7 +1495,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.4.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -1407,17 +1504,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.1.0", - "indexmap 2.4.0", + "http 1.2.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -1445,6 +1542,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "hashlink" version = "0.9.1" @@ -1508,13 +1611,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[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]] @@ -1539,9 +1651,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1566,7 +1678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -1577,16 +1689,16 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1611,9 +1723,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -1635,17 +1747,18 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.6", - "http 1.1.0", + "h2 0.4.7", + "http 1.2.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1655,20 +1768,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.4.1", + "http 1.2.0", + "hyper 1.5.1", "hyper-util", - "rustls 0.23.12", + "rustls 0.23.20", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tower-service", - "webpki-roots 0.26.3", + "webpki-roots 0.26.7", ] [[package]] @@ -1677,12 +1790,25 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.30", + "hyper 0.14.31", "pin-project-lite", "tokio", "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.5.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1690,7 +1816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.30", + "hyper 0.14.31", "native-tls", "tokio", "tokio-native-tls", @@ -1698,29 +1824,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.1", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1739,6 +1864,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1757,12 +2000,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1777,12 +2031,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] @@ -1806,9 +2060,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_terminal_polyfill" @@ -1825,18 +2079,28 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1848,9 +2112,54 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] [[package]] name = "libsqlite3-sys" @@ -1889,7 +2198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "767f388e50251da71f95a3737d6db32c9729f9de6427a54fa92bb994d04d793f" dependencies = [ "bech32 0.9.1", - "bitcoin 0.32.4", + "bitcoin 0.32.5", "lightning-invoice 0.32.0", "lightning-types", ] @@ -1928,7 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ab9f6ea77e20e3129235e62a2e6bd64ed932363df104e864ee65ccffb54a8f" dependencies = [ "bech32 0.9.1", - "bitcoin 0.32.4", + "bitcoin 0.32.5", "lightning-types", ] @@ -1939,7 +2248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1083b8d9137000edf3bfcb1ff011c0d25e0cdd2feb98cc21d6765e64a494148f" dependencies = [ "bech32 0.9.1", - "bitcoin 0.32.4", + "bitcoin 0.32.5", "hex-conservative 0.2.1", ] @@ -1949,6 +2258,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -1977,7 +2292,7 @@ dependencies = [ "getrandom", "qr_code", "rand", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2004,13 +2319,13 @@ dependencies = [ "lwk_common", "lwk_containers", "rand", - "reqwest 0.12.7", + "reqwest 0.12.9", "serde", "serde_bytes", "serde_cbor", "serde_json", "tempfile", - "thiserror", + "thiserror 1.0.69", "tracing", "wasm-timer", ] @@ -2026,7 +2341,7 @@ dependencies = [ "elements-miniscript", "lwk_common", "lwk_jade", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2046,10 +2361,10 @@ dependencies = [ "once_cell", "rand", "regex-lite", - "reqwest 0.12.7", + "reqwest 0.12.9", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tracing", ] @@ -2066,7 +2381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2098,15 +2413,6 @@ dependencies = [ "bitcoin-internals 0.2.0", ] -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -2118,11 +2424,10 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -2134,6 +2439,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + [[package]] name = "native-tls" version = "0.2.12" @@ -2227,9 +2538,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] @@ -2245,9 +2556,13 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -2257,9 +2572,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -2278,7 +2593,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -2289,18 +2604,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -2363,7 +2678,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -2387,34 +2702,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.4.0", + "indexmap 2.7.0", ] [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -2424,9 +2739,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polyval" @@ -2440,6 +2755,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2465,11 +2786,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.90", +] + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2481,7 +2812,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive 0.13.4", ] [[package]] @@ -2492,20 +2833,40 @@ checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes", "heck 0.4.1", - "itertools", + "itertools 0.10.5", "lazy_static", "log", - "multimap", + "multimap 0.8.3", "petgraph", - "prettyplease", - "prost", - "prost-types", + "prettyplease 0.1.25", + "prost 0.11.9", + "prost-types 0.11.9", "regex", "syn 1.0.109", "tempfile", "which", ] +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck 0.5.0", + "itertools 0.13.0", + "log", + "multimap 0.10.0", + "once_cell", + "petgraph", + "prettyplease 0.2.25", + "prost 0.13.4", + "prost-types 0.13.4", + "regex", + "syn 2.0.90", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -2513,19 +2874,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "prost-types" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "prost", + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost 0.13.4", ] [[package]] @@ -2539,9 +2922,9 @@ dependencies = [ [[package]] name = "qrcode-rs" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312bbb0be6e357c1f8191d3c3b0f892bf2b80240d379d08d92355f1dee6afa74" +checksum = "e2ead539a9857a2a4e45ec7285812734740c12014cb62e3d1567b1676106afcf" [[package]] name = "querystring" @@ -2557,57 +2940,61 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ "bytes", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.12", + "rustls 0.23.20", "socket2", - "thiserror", + "thiserror 2.0.6", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.6" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", + "getrandom", "rand", "ring 0.17.8", "rustc-hash", - "rustls 0.23.12", + "rustls 0.23.20", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.6", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.4" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +checksum = "52cd4b1eff68bf27940dd39811292c49e007f4d0b4c357358dc9b0197be6b527" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2663,9 +3050,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.6.0", ] @@ -2719,7 +3106,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-tls", "ipnet", "js-sys", @@ -2744,9 +3131,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.7" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", @@ -2754,11 +3141,11 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.6", - "http 1.1.0", + "h2 0.4.7", + "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.1", "hyper-rustls", "hyper-util", "ipnet", @@ -2769,22 +3156,22 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.12", - "rustls-pemfile 2.1.3", + "rustls 0.23.20", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "system-configuration", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.26.3", + "webpki-roots 0.26.7", "windows-registry", ] @@ -2850,9 +3237,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rusticata-macros" @@ -2865,15 +3252,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2902,14 +3289,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ + "log", "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.6", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -2937,19 +3325,21 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -2963,9 +3353,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -2974,9 +3364,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rustyline" @@ -3009,7 +3399,7 @@ checksum = "e5af959c8bf6af1aff6d2b463a57f71aae53d1332da58419e30ad8dc7011d951" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -3020,11 +3410,11 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3046,7 +3436,7 @@ dependencies = [ [[package]] name = "sdk-common" version = "0.6.2" -source = "git+https://github.com/breez/breez-sdk?rev=2208dd5530908d5cf7b607f34f80155669ab299b#2208dd5530908d5cf7b607f34f80155669ab299b" +source = "git+https://github.com/breez/breez-sdk?rev=f77208acd34d74b571388889e856444908c59a85#f77208acd34d74b571388889e856444908c59a85" dependencies = [ "aes 0.8.4", "anyhow", @@ -3060,17 +3450,17 @@ dependencies = [ "lightning-invoice 0.26.0", "log", "percent-encoding", - "prost", + "prost 0.11.9", "querystring", "regex", "reqwest 0.11.20", "serde", "serde_json", "strum_macros", - "thiserror", + "thiserror 1.0.69", "tokio", - "tonic", - "tonic-build", + "tonic 0.8.3", + "tonic-build 0.8.4", "url", "urlencoding", ] @@ -3197,11 +3587,17 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + [[package]] name = "serde" -version = "1.0.208" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] @@ -3227,20 +3623,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -3290,7 +3686,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3301,7 +3697,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3336,9 +3732,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3356,6 +3752,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.10.0" @@ -3384,7 +3786,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -3406,9 +3808,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.75" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -3423,9 +3825,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -3438,14 +3840,14 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "system-configuration" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -3464,9 +3866,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -3503,22 +3905,42 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +dependencies = [ + "thiserror-impl 2.0.6", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -3532,9 +3954,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3553,14 +3975,24 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -3612,7 +4044,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] @@ -3638,20 +4070,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.12", - "rustls-pki-types", + "rustls 0.23.20", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3675,9 +4106,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3694,7 +4125,7 @@ checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.13.1", "bytes", "futures-core", @@ -3702,19 +4133,19 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", - "hyper-timeout", + "hyper 0.14.31", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", - "prost", - "prost-derive", + "prost 0.11.9", + "prost-derive 0.11.9", "rustls-native-certs", "rustls-pemfile 1.0.4", "tokio", "tokio-rustls 0.23.4", "tokio-stream", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -3722,19 +4153,66 @@ dependencies = [ "webpki-roots 0.22.6", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-timeout 0.5.2", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.4", + "rustls-pemfile 2.2.0", + "socket2", + "tokio", + "tokio-rustls 0.26.1", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", + "webpki-roots 0.26.7", +] + [[package]] name = "tonic-build" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" dependencies = [ - "prettyplease", + "prettyplease 0.1.25", "proc-macro2", - "prost-build", + "prost-build 0.11.9", "quote", "syn 1.0.109", ] +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease 0.2.25", + "proc-macro2", + "prost-build 0.13.4", + "prost-types 0.13.4", + "quote", + "syn 2.0.90", +] + [[package]] name = "tower" version = "0.4.13" @@ -3755,6 +4233,20 @@ dependencies = [ "tracing", ] +[[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", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3769,9 +4261,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3780,20 +4272,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -3823,13 +4315,13 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http 1.2.0", "httparse", "log", "native-tls", "rand", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -3842,15 +4334,15 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" @@ -3863,15 +4355,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "universal-hash" @@ -3897,31 +4389,31 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.8.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "flate2", "log", "native-tls", "once_cell", - "rustls 0.21.12", - "rustls-webpki 0.101.7", + "rustls 0.23.20", + "rustls-pki-types", "serde", "serde_json", "url", - "webpki-roots 0.25.4", + "webpki-roots 0.26.7", ] [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", ] @@ -3937,6 +4429,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3972,9 +4476,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -3983,36 +4487,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4020,22 +4524,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wasm-timer" @@ -4054,9 +4558,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -4089,9 +4603,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -4336,6 +4850,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x509-parser" version = "0.16.0" @@ -4349,10 +4875,34 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + [[package]] name = "zbase32" version = "0.1.2" @@ -4377,7 +4927,28 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.90", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", ] [[package]] @@ -4385,3 +4956,25 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 1101724b8..48ba70156 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -98,6 +98,10 @@ pub(crate) enum Command { #[clap(name = "filter", short = 'r', long = "filter")] filters: Option>, + /// The optional payment state. Either "pending", "complete", "failed", "pendingrefund" or "refundable" + #[clap(name = "state", short = 's', long = "state")] + states: Option>, + /// The optional from unix timestamp #[clap(name = "from_timestamp", short = 'f', long = "from")] from_timestamp: Option, @@ -477,6 +481,7 @@ pub(crate) async fn handle_command( } Command::ListPayments { filters, + states, from_timestamp, to_timestamp, limit, @@ -493,6 +498,7 @@ pub(crate) async fn handle_command( let payments = sdk .list_payments(&ListPaymentsRequest { filters, + states, from_timestamp, to_timestamp, limit, @@ -550,7 +556,7 @@ pub(crate) async fn handle_command( command_result!("Rescanned successfully") } Command::Sync => { - sdk.sync().await?; + sdk.sync(false).await?; command_result!("Synced successfully") } Command::RecommendedFees => { diff --git a/lib/Cargo.lock b/lib/Cargo.lock index c8494ffc5..557adfdbb 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -188,6 +188,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -239,7 +245,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -298,7 +304,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "synstructure", ] @@ -310,7 +316,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -332,7 +338,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -343,7 +349,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -382,7 +388,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", @@ -398,7 +404,34 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.1", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -420,6 +453,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -750,6 +803,7 @@ dependencies = [ "bip39", "boltz-client", "chrono", + "ecies", "electrum-client", "env_logger 0.11.5", "flutter_rust_bridge", @@ -764,12 +818,14 @@ dependencies = [ "lwk_wollet", "openssl", "paste", + "prost 0.13.4", "reqwest 0.11.20", "rusqlite", "rusqlite_migration", "sdk-common", "security-framework", "security-framework-sys", + "semver", "serde", "serde_json", "strum", @@ -780,6 +836,8 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "tonic 0.12.3", + "tonic-build 0.12.3", "url", "uuid", "x509-parser", @@ -984,7 +1042,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1052,6 +1110,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1139,7 +1209,7 @@ checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1165,6 +1235,15 @@ dependencies = [ "powerfmt", ] +[[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" @@ -1184,7 +1263,24 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", +] + +[[package]] +name = "ecies" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0206e602d2645ec8b24ed8307fadbc6c3110e2b11ab2f806fc02fee49327079" +dependencies = [ + "getrandom", + "hkdf", + "libsecp256k1", + "once_cell", + "openssl", + "parking_lot 0.12.3", + "rand_core 0.6.4", + "sha2", + "wasm-bindgen", ] [[package]] @@ -1378,7 +1474,7 @@ dependencies = [ "md-5", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1482,7 +1578,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -1536,9 +1632,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -1692,13 +1788,22 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[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]] @@ -1830,6 +1935,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1867,6 +1973,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1882,9 +2001,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -1895,7 +2014,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -2055,6 +2173,51 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "libsqlite3-sys" version = "0.28.0" @@ -2269,7 +2432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2432,6 +2595,10 @@ name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "oneshot-uniffi" @@ -2468,7 +2635,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2609,7 +2776,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -2648,6 +2815,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2673,6 +2846,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +dependencies = [ + "proc-macro2", + "syn 2.0.87", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2713,7 +2896,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +dependencies = [ + "bytes", + "prost-derive 0.13.4", ] [[package]] @@ -2729,15 +2922,35 @@ dependencies = [ "log", "multimap", "petgraph", - "prettyplease", - "prost", - "prost-types", + "prettyplease 0.1.25", + "prost 0.11.9", + "prost-types 0.11.9", "regex", "syn 1.0.109", "tempfile", "which", ] +[[package]] +name = "prost-build" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b" +dependencies = [ + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease 0.2.25", + "prost 0.13.4", + "prost-types 0.13.4", + "regex", + "syn 2.0.87", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -2751,13 +2964,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "prost-types" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "prost", + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" +dependencies = [ + "prost 0.13.4", ] [[package]] @@ -3168,6 +3403,7 @@ version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ + "log", "once_cell", "ring 0.17.8", "rustls-pki-types", @@ -3278,7 +3514,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3294,7 +3530,7 @@ dependencies = [ [[package]] name = "sdk-common" version = "0.6.2" -source = "git+https://github.com/breez/breez-sdk?rev=2208dd5530908d5cf7b607f34f80155669ab299b#2208dd5530908d5cf7b607f34f80155669ab299b" +source = "git+https://github.com/breez/breez-sdk?rev=f77208acd34d74b571388889e856444908c59a85#f77208acd34d74b571388889e856444908c59a85" dependencies = [ "aes 0.8.4", "anyhow", @@ -3308,7 +3544,7 @@ dependencies = [ "lightning-invoice 0.26.0", "log", "percent-encoding", - "prost", + "prost 0.11.9", "querystring", "regex", "reqwest 0.11.20", @@ -3317,8 +3553,8 @@ dependencies = [ "strum_macros", "thiserror", "tokio", - "tonic", - "tonic-build", + "tonic 0.8.3", + "tonic-build 0.8.4", "url", "urlencoding", ] @@ -3490,7 +3726,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3547,7 +3783,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3558,7 +3794,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3653,7 +3889,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3675,9 +3911,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -3707,7 +3943,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3803,7 +4039,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3897,7 +4133,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -3988,7 +4224,7 @@ checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.13.1", "bytes", "futures-core", @@ -3997,18 +4233,18 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", - "hyper-timeout", + "hyper-timeout 0.4.1", "percent-encoding", "pin-project", - "prost", - "prost-derive", + "prost 0.11.9", + "prost-derive 0.11.9", "rustls-native-certs", "rustls-pemfile 1.0.4", "tokio", "tokio-rustls 0.23.4", "tokio-stream", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -4016,19 +4252,66 @@ dependencies = [ "webpki-roots 0.22.6", ] +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-timeout 0.5.2", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.4", + "rustls-pemfile 2.1.3", + "socket2", + "tokio", + "tokio-rustls 0.26.0", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", + "webpki-roots 0.26.5", +] + [[package]] name = "tonic-build" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4" dependencies = [ - "prettyplease", + "prettyplease 0.1.25", "proc-macro2", - "prost-build", + "prost-build 0.11.9", "quote", "syn 1.0.109", ] +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease 0.2.25", + "proc-macro2", + "prost-build 0.13.4", + "prost-types 0.13.4", + "quote", + "syn 2.0.87", +] + [[package]] name = "tower" version = "0.4.13" @@ -4049,6 +4332,20 @@ dependencies = [ "tracing", ] +[[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.1", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -4080,7 +4377,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4297,7 +4594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] @@ -4364,7 +4661,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.87", "toml", "uniffi_build 0.25.3", "uniffi_meta 0.25.3", @@ -4561,7 +4858,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -4595,7 +4892,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4955,7 +5252,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.87", ] [[package]] diff --git a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h index 75b054346..0a2d51351 100644 --- a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h +++ b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h @@ -81,6 +81,11 @@ typedef struct wire_cst_list_payment_type { int32_t len; } wire_cst_list_payment_type; +typedef struct wire_cst_list_payment_state { + int32_t *ptr; + int32_t len; +} wire_cst_list_payment_state; + typedef struct wire_cst_ListPaymentDetails_Liquid { struct wire_cst_list_prim_u_8_strict *destination; } wire_cst_ListPaymentDetails_Liquid; @@ -101,6 +106,7 @@ typedef struct wire_cst_list_payment_details { typedef struct wire_cst_list_payments_request { struct wire_cst_list_payment_type *filters; + struct wire_cst_list_payment_state *states; int64_t *from_timestamp; int64_t *to_timestamp; uint32_t *offset; @@ -231,6 +237,18 @@ typedef struct wire_cst_send_destination { union SendDestinationKind kind; } wire_cst_send_destination; +typedef struct wire_cst_ln_url_pay_request_data { + struct wire_cst_list_prim_u_8_strict *callback; + uint64_t min_sendable; + uint64_t max_sendable; + struct wire_cst_list_prim_u_8_strict *metadata_str; + uint16_t comment_allowed; + struct wire_cst_list_prim_u_8_strict *domain; + bool allows_nostr; + struct wire_cst_list_prim_u_8_strict *nostr_pubkey; + struct wire_cst_list_prim_u_8_strict *ln_address; +} wire_cst_ln_url_pay_request_data; + typedef struct wire_cst_aes_success_action_data { struct wire_cst_list_prim_u_8_strict *description; struct wire_cst_list_prim_u_8_strict *ciphertext; @@ -273,6 +291,8 @@ typedef struct wire_cst_success_action { typedef struct wire_cst_prepare_ln_url_pay_response { struct wire_cst_send_destination destination; uint64_t fees_sat; + struct wire_cst_ln_url_pay_request_data data; + struct wire_cst_list_prim_u_8_strict *comment; struct wire_cst_success_action *success_action; } wire_cst_prepare_ln_url_pay_response; @@ -310,18 +330,6 @@ typedef struct wire_cst_prepare_buy_bitcoin_request { uint64_t amount_sat; } wire_cst_prepare_buy_bitcoin_request; -typedef struct wire_cst_ln_url_pay_request_data { - struct wire_cst_list_prim_u_8_strict *callback; - uint64_t min_sendable; - uint64_t max_sendable; - struct wire_cst_list_prim_u_8_strict *metadata_str; - uint16_t comment_allowed; - struct wire_cst_list_prim_u_8_strict *domain; - bool allows_nostr; - struct wire_cst_list_prim_u_8_strict *nostr_pubkey; - struct wire_cst_list_prim_u_8_strict *ln_address; -} wire_cst_ln_url_pay_request_data; - typedef struct wire_cst_prepare_ln_url_pay_request { struct wire_cst_ln_url_pay_request_data data; uint64_t amount_msat; @@ -405,6 +413,62 @@ typedef struct wire_cst_binding_event_listener { struct wire_cst_list_prim_u_8_strict *stream; } wire_cst_binding_event_listener; +typedef struct wire_cst_aes_success_action_data_decrypted { + struct wire_cst_list_prim_u_8_strict *description; + struct wire_cst_list_prim_u_8_strict *plaintext; +} wire_cst_aes_success_action_data_decrypted; + +typedef struct wire_cst_AesSuccessActionDataResult_Decrypted { + struct wire_cst_aes_success_action_data_decrypted *data; +} wire_cst_AesSuccessActionDataResult_Decrypted; + +typedef struct wire_cst_AesSuccessActionDataResult_ErrorStatus { + struct wire_cst_list_prim_u_8_strict *reason; +} wire_cst_AesSuccessActionDataResult_ErrorStatus; + +typedef union AesSuccessActionDataResultKind { + struct wire_cst_AesSuccessActionDataResult_Decrypted Decrypted; + struct wire_cst_AesSuccessActionDataResult_ErrorStatus ErrorStatus; +} AesSuccessActionDataResultKind; + +typedef struct wire_cst_aes_success_action_data_result { + int32_t tag; + union AesSuccessActionDataResultKind kind; +} wire_cst_aes_success_action_data_result; + +typedef struct wire_cst_SuccessActionProcessed_Aes { + struct wire_cst_aes_success_action_data_result *result; +} wire_cst_SuccessActionProcessed_Aes; + +typedef struct wire_cst_SuccessActionProcessed_Message { + struct wire_cst_message_success_action_data *data; +} wire_cst_SuccessActionProcessed_Message; + +typedef struct wire_cst_SuccessActionProcessed_Url { + struct wire_cst_url_success_action_data *data; +} wire_cst_SuccessActionProcessed_Url; + +typedef union SuccessActionProcessedKind { + struct wire_cst_SuccessActionProcessed_Aes Aes; + struct wire_cst_SuccessActionProcessed_Message Message; + struct wire_cst_SuccessActionProcessed_Url Url; +} SuccessActionProcessedKind; + +typedef struct wire_cst_success_action_processed { + int32_t tag; + union SuccessActionProcessedKind kind; +} wire_cst_success_action_processed; + +typedef struct wire_cst_ln_url_info { + struct wire_cst_list_prim_u_8_strict *ln_address; + struct wire_cst_list_prim_u_8_strict *lnurl_pay_comment; + struct wire_cst_list_prim_u_8_strict *lnurl_pay_domain; + struct wire_cst_list_prim_u_8_strict *lnurl_pay_metadata; + struct wire_cst_success_action_processed *lnurl_pay_success_action; + struct wire_cst_success_action *lnurl_pay_unprocessed_success_action; + struct wire_cst_list_prim_u_8_strict *lnurl_withdraw_endpoint; +} wire_cst_ln_url_info; + typedef struct wire_cst_PaymentDetails_Lightning { struct wire_cst_list_prim_u_8_strict *swap_id; struct wire_cst_list_prim_u_8_strict *description; @@ -412,6 +476,7 @@ typedef struct wire_cst_PaymentDetails_Lightning { struct wire_cst_list_prim_u_8_strict *bolt11; struct wire_cst_list_prim_u_8_strict *bolt12_offer; struct wire_cst_list_prim_u_8_strict *payment_hash; + struct wire_cst_ln_url_info *lnurl_info; struct wire_cst_list_prim_u_8_strict *refund_tx_id; uint64_t *refund_tx_amount_sat; } wire_cst_PaymentDetails_Lightning; @@ -509,6 +574,7 @@ typedef struct wire_cst_config { int32_t network; uint64_t payment_timeout_sec; uint32_t zero_conf_min_fee_rate_msat; + struct wire_cst_list_prim_u_8_strict *sync_service_url; uint64_t *zero_conf_max_amount_sat; struct wire_cst_list_prim_u_8_strict *breez_api_key; struct wire_cst_list_external_input_parser *external_input_parsers; @@ -520,29 +586,6 @@ typedef struct wire_cst_connect_request { struct wire_cst_list_prim_u_8_strict *mnemonic; } wire_cst_connect_request; -typedef struct wire_cst_aes_success_action_data_decrypted { - struct wire_cst_list_prim_u_8_strict *description; - struct wire_cst_list_prim_u_8_strict *plaintext; -} wire_cst_aes_success_action_data_decrypted; - -typedef struct wire_cst_AesSuccessActionDataResult_Decrypted { - struct wire_cst_aes_success_action_data_decrypted *data; -} wire_cst_AesSuccessActionDataResult_Decrypted; - -typedef struct wire_cst_AesSuccessActionDataResult_ErrorStatus { - struct wire_cst_list_prim_u_8_strict *reason; -} wire_cst_AesSuccessActionDataResult_ErrorStatus; - -typedef union AesSuccessActionDataResultKind { - struct wire_cst_AesSuccessActionDataResult_Decrypted Decrypted; - struct wire_cst_AesSuccessActionDataResult_ErrorStatus ErrorStatus; -} AesSuccessActionDataResultKind; - -typedef struct wire_cst_aes_success_action_data_result { - int32_t tag; - union AesSuccessActionDataResultKind kind; -} wire_cst_aes_success_action_data_result; - typedef struct wire_cst_bitcoin_address_data { struct wire_cst_list_prim_u_8_strict *address; int32_t network; @@ -560,29 +603,6 @@ typedef struct wire_cst_ln_url_pay_error_data { struct wire_cst_list_prim_u_8_strict *reason; } wire_cst_ln_url_pay_error_data; -typedef struct wire_cst_SuccessActionProcessed_Aes { - struct wire_cst_aes_success_action_data_result *result; -} wire_cst_SuccessActionProcessed_Aes; - -typedef struct wire_cst_SuccessActionProcessed_Message { - struct wire_cst_message_success_action_data *data; -} wire_cst_SuccessActionProcessed_Message; - -typedef struct wire_cst_SuccessActionProcessed_Url { - struct wire_cst_url_success_action_data *data; -} wire_cst_SuccessActionProcessed_Url; - -typedef union SuccessActionProcessedKind { - struct wire_cst_SuccessActionProcessed_Aes Aes; - struct wire_cst_SuccessActionProcessed_Message Message; - struct wire_cst_SuccessActionProcessed_Url Url; -} SuccessActionProcessedKind; - -typedef struct wire_cst_success_action_processed { - int32_t tag; - union SuccessActionProcessedKind kind; -} wire_cst_success_action_processed; - typedef struct wire_cst_ln_url_pay_success_data { struct wire_cst_payment payment; struct wire_cst_success_action_processed *success_action; @@ -1231,6 +1251,8 @@ struct wire_cst_ln_url_auth_request_data *frbgen_breez_liquid_cst_new_box_autoad struct wire_cst_ln_url_error_data *frbgen_breez_liquid_cst_new_box_autoadd_ln_url_error_data(void); +struct wire_cst_ln_url_info *frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info(void); + struct wire_cst_ln_url_pay_error_data *frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_error_data(void); struct wire_cst_ln_url_pay_request *frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_request(void); @@ -1303,6 +1325,8 @@ struct wire_cst_list_localized_name *frbgen_breez_liquid_cst_new_list_localized_ struct wire_cst_list_payment *frbgen_breez_liquid_cst_new_list_payment(int32_t len); +struct wire_cst_list_payment_state *frbgen_breez_liquid_cst_new_list_payment_state(int32_t len); + struct wire_cst_list_payment_type *frbgen_breez_liquid_cst_new_list_payment_type(int32_t len); struct wire_cst_list_prim_u_8_strict *frbgen_breez_liquid_cst_new_list_prim_u_8_strict(int32_t len); @@ -1337,6 +1361,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_offer); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_url_auth_request_data); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_url_error_data); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_error_data); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_request_data); @@ -1373,6 +1398,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_locale_overrides); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_localized_name); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_payment); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_payment_state); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_payment_type); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_prim_u_8_strict); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_rate); diff --git a/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h b/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h index 4295729cb..0f02f4e88 100644 --- a/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h +++ b/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h @@ -408,6 +408,12 @@ uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_signer_slip77_master_b ); uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_signer_hmac_sha256(void +); +uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encrypt(void + +); +uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decrypt(void + ); uint32_t ffi_breez_sdk_liquid_bindings_uniffi_contract_version(void diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift index 49ea38709..fd70f715b 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift @@ -54,7 +54,7 @@ class SwapUpdatedTask : TaskProtocol { switch details { case let .bitcoin(swapId, _, _, _): return swapId - case let .lightning(swapId, _, _, _, _, _, _, _): + case let .lightning(swapId, _, _, _, _, _, _, _, _): return swapId default: break diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 149df52d6..ba42b3a86 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -332,6 +332,7 @@ dictionary Config { LiquidNetwork network; u64 payment_timeout_sec; u32 zero_conf_min_fee_rate_msat; + string sync_service_url; string? breez_api_key; string? cache_dir; u64? zero_conf_max_amount_sat; @@ -389,6 +390,8 @@ dictionary PrepareLnUrlPayRequest { dictionary PrepareLnUrlPayResponse { SendDestination destination; u64 fees_sat; + LnUrlPayRequestData data; + string? comment = null; SuccessAction? success_action = null; }; @@ -519,6 +522,7 @@ dictionary RestoreRequest { dictionary ListPaymentsRequest { sequence? filters = null; + sequence? states = null; i64? from_timestamp = null; i64? to_timestamp = null; u32? offset = null; @@ -537,9 +541,19 @@ interface GetPaymentRequest { Lightning(string payment_hash); }; +dictionary LnUrlInfo { + string? ln_address; + string? lnurl_pay_comment; + string? lnurl_pay_domain; + string? lnurl_pay_metadata; + SuccessActionProcessed? lnurl_pay_success_action; + SuccessAction? lnurl_pay_unprocessed_success_action; + string? lnurl_withdraw_endpoint; +}; + [Enum] interface PaymentDetails { - Lightning(string swap_id, string description, string? preimage, string? bolt11, string? bolt12_offer, string? payment_hash, string? refund_tx_id, u64? refund_tx_amount_sat); + Lightning(string swap_id, string description, string? preimage, string? bolt11, string? bolt12_offer, string? payment_hash, LnUrlInfo? lnurl_info, string? refund_tx_id, u64? refund_tx_amount_sat); Liquid(string destination, string description); Bitcoin(string swap_id, string description, string? refund_tx_id, u64? refund_tx_amount_sat); }; @@ -677,6 +691,12 @@ callback interface Signer { [Throws=SignerError] sequence hmac_sha256(sequence msg, string derivation_path); + + [Throws=SignerError] + sequence ecies_encrypt(sequence msg); + + [Throws=SignerError] + sequence ecies_decrypt(sequence msg); }; interface BindingLiquidSdk { diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index d3db7039d..995fc3b1f 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -230,7 +230,7 @@ impl BindingLiquidSdk { } pub fn sync(&self) -> SdkResult<()> { - rt().block_on(self.sdk.sync()).map_err(Into::into) + rt().block_on(self.sdk.sync(false)).map_err(Into::into) } pub fn recommended_fees(&self) -> SdkResult { diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 63af0eaa4..26af69232 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -32,7 +32,7 @@ lwk_wollet = { git = "https://github.com/dangeross/lwk", branch = "savage-full-s #lwk_wollet = "0.7.0" rusqlite = { version = "0.31", features = ["backup", "bundled"] } rusqlite_migration = "1.0" -sdk-common = { git = "https://github.com/breez/breez-sdk", rev = "2208dd5530908d5cf7b607f34f80155669ab299b", features = ["liquid"] } +sdk-common = { git = "https://github.com/breez/breez-sdk", rev = "f77208acd34d74b571388889e856444908c59a85", features = ["liquid"] } serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.116" strum = "0.25" @@ -54,9 +54,13 @@ electrum-client = { version = "0.19.0" } zbase32 = "0.1.2" x509-parser = { version = "0.16.0" } tempfile = "3" +prost = "0.13.3" +ecies = "0.2.7" +semver = "1.0.23" +lazy_static = "1.5.0" +tonic = { version = "0.12.3", features = ["tls", "tls-webpki-roots"] } [dev-dependencies] -lazy_static = "1.5.0" paste = "1.0.15" tempdir = "0.3.7" uuid = { version = "1.8.0", features = ["v4"] } @@ -64,6 +68,7 @@ uuid = { version = "1.8.0", features = ["v4"] } [build-dependencies] anyhow = { version = "1.0.79", features = ["backtrace"] } glob = "0.3.1" +tonic-build = "0.12.3" # Pin these versions to fix iOS build issues [target.'cfg(target_os = "ios")'.build-dependencies] diff --git a/lib/core/build.rs b/lib/core/build.rs index a5be78907..d93dbcb0e 100644 --- a/lib/core/build.rs +++ b/lib/core/build.rs @@ -1,6 +1,8 @@ use anyhow::*; use glob::glob; use std::env; +use std::os::unix::process::CommandExt as _; +use std::process::Command; use std::result::Result::Ok; /// Adds a temporary workaround for an issue with the Rust compiler and Android @@ -32,7 +34,21 @@ fn setup_x86_64_android_workaround() { } } +fn compile_protos() -> Result<()> { + tonic_build::configure() + .build_server(false) + .out_dir("src/sync/model") + .compile_protos(&["src/sync/proto/sync.proto"], &["src/sync/proto"])?; + Command::new("rustfmt") + .arg("--edition") + .arg("2021") + .arg("src/sync/model/sync.rs") + .exec(); + Ok(()) +} + fn main() -> Result<()> { setup_x86_64_android_workaround(); + compile_protos()?; Ok(()) } diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index 13abe904b..3717cb96a 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -77,7 +77,7 @@ pub struct BindingLiquidSdk { impl BindingLiquidSdk { pub async fn get_info(&self) -> Result { - self.sdk.get_info().await.map_err(Into::into) + self.sdk.get_info().await } #[frb(sync)] @@ -256,7 +256,7 @@ impl BindingLiquidSdk { #[frb(name = "sync")] pub async fn sync(&self) -> Result<(), SdkError> { - self.sdk.sync().await.map_err(Into::into) + self.sdk.sync(false).await.map_err(Into::into) } pub async fn recommended_fees(&self) -> Result { diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index f144d43a9..5cfe2bb97 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -1,11 +1,11 @@ -use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; +use async_trait::async_trait; use boltz_client::{ boltz::{self}, swaps::boltz::{ChainSwapStates, CreateChainResponse, SwapUpdateTxDetails}, - Address, ElementsLockTime, LockTime, Secp256k1, Serialize, ToHex, + ElementsLockTime, Secp256k1, Serialize, ToHex, }; use futures_util::TryFutureExt; use log::{debug, error, info, warn}; @@ -14,9 +14,9 @@ use lwk_wollet::{ hashes::hex::DisplayHex, History, }; -use tokio::sync::{broadcast, watch, Mutex}; -use tokio::time::MissedTickBehavior; +use tokio::sync::{broadcast, Mutex}; +use crate::model::{BlockListener, ChainSwapUpdate}; use crate::{ chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService}, ensure_sdk, @@ -27,7 +27,6 @@ use crate::{ PaymentTxData, PaymentType, Swap, SwapScriptV2, Transaction as SdkTransaction, }, persist::Persister, - sdk::CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS, swapper::Swapper, utils, wallet::OnchainWallet, @@ -46,6 +45,24 @@ pub(crate) struct ChainSwapHandler { subscription_notifier: broadcast::Sender, } +#[async_trait] +impl BlockListener for ChainSwapHandler { + async fn on_bitcoin_block(&self, height: u32) { + if let Err(e) = self.claim_outgoing(height).await { + error!("Error claiming outgoing: {e:?}"); + } + } + + async fn on_liquid_block(&self, height: u32) { + if let Err(e) = self.refund_outgoing(height).await { + warn!("Error refunding outgoing: {e:?}"); + } + if let Err(e) = self.claim_incoming(height).await { + error!("Error claiming incoming: {e:?}"); + } + } +} + impl ChainSwapHandler { pub(crate) fn new( config: Config, @@ -67,38 +84,6 @@ impl ChainSwapHandler { }) } - pub(crate) async fn start(self: Arc, mut shutdown: watch::Receiver<()>) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut bitcoin_rescan_interval = tokio::time::interval(Duration::from_secs(60 * 10)); - let mut liquid_rescan_interval = tokio::time::interval(Duration::from_secs(60)); - bitcoin_rescan_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); - liquid_rescan_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); - - loop { - tokio::select! { - _ = bitcoin_rescan_interval.tick() => { - if let Err(e) = cloned.rescan_incoming_user_lockup_txs(false).await { - error!("Error checking incoming user txs: {e:?}"); - } - if let Err(e) = cloned.rescan_outgoing_claim_txs().await { - error!("Error checking outgoing server txs: {e:?}"); - } - }, - _ = liquid_rescan_interval.tick() => { - if let Err(e) = cloned.rescan_incoming_server_lockup_txs().await { - error!("Error checking incoming server txs: {e:?}"); - } - }, - _ = shutdown.changed() => { - info!("Received shutdown signal, exiting chain swap loop"); - return; - } - } - } - }); - } - pub(crate) fn subscribe_payment_updates(&self) -> broadcast::Receiver { self.subscription_notifier.subscribe() } @@ -106,10 +91,25 @@ impl ChainSwapHandler { /// Handles status updates from Boltz for Chain swaps pub(crate) async fn on_new_status(&self, update: &boltz::Update) -> Result<()> { let id = &update.id; - let swap = self - .persister - .fetch_chain_swap_by_id(id)? - .ok_or(anyhow!("No ongoing Chain Swap found for ID {id}"))?; + let swap = self.fetch_chain_swap_by_id(id)?; + + if let Some(sync_state) = self.persister.get_sync_state_by_data_id(&swap.id)? { + if !sync_state.is_local { + let status = &update.status; + let swap_state = ChainSwapStates::from_str(status) + .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; + + match swap_state { + // If the swap is not local (pulled from real-time sync) we do not claim twice + ChainSwapStates::TransactionServerMempool + | ChainSwapStates::TransactionServerConfirmed => { + log::debug!("Received {swap_state:?} for non-local Chain swap {id} from status stream, skipping update."); + return Ok(()); + } + _ => {} + } + } + } match swap.direction { Direction::Incoming => self.on_new_incoming_status(&swap, update).await, @@ -117,126 +117,49 @@ impl ChainSwapHandler { } } - pub(crate) async fn rescan_incoming_user_lockup_txs( - &self, - ignore_monitoring_block_height: bool, - ) -> Result<()> { - let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32; + async fn claim_incoming(&self, height: u32) -> Result<()> { let chain_swaps: Vec = self .persister .list_chain_swaps()? .into_iter() - .filter(|s| s.direction == Direction::Incoming) + .filter(|s| { + s.direction == Direction::Incoming && s.state == Pending && s.claim_tx_id.is_none() + }) .collect(); info!( - "Rescanning {} incoming Chain Swap(s) user lockup txs at height {}", + "Rescanning {} incoming Chain Swap(s) server lockup txs at height {}", chain_swaps.len(), - current_height + height ); for swap in chain_swaps { - if let Err(e) = self - .rescan_incoming_chain_swap_user_lockup_tx( - &swap, - current_height, - ignore_monitoring_block_height, - ) - .await - { + if let Err(e) = self.claim_confirmed_server_lockup(&swap).await { error!( - "Error rescanning user lockup of incoming Chain Swap {}: {e:?}", - swap.id - ); - } - } - Ok(()) - } - - /// ### Arguments - /// - `swap`: the swap being rescanned - /// - `current_height`: the tip - /// - `ignore_monitoring_block_height`: if true, it rescans an expired swap even after the - /// cutoff monitoring block height - async fn rescan_incoming_chain_swap_user_lockup_tx( - &self, - swap: &ChainSwap, - current_height: u32, - ignore_monitoring_block_height: bool, - ) -> Result<()> { - let monitoring_block_height = - swap.timeout_block_height + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS; - let is_swap_expired = current_height > swap.timeout_block_height; - let is_monitoring_expired = match ignore_monitoring_block_height { - true => false, - false => current_height > monitoring_block_height, - }; - - if (is_swap_expired && !is_monitoring_expired) || swap.state == RefundPending { - let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?; - let script_balance = self - .bitcoin_chain_service - .lock() - .await - .script_get_balance(script_pubkey.as_script())?; - info!( - "Incoming Chain Swap {} has {} confirmed and {} unconfirmed sats", - swap.id, script_balance.confirmed, script_balance.unconfirmed - ); - - if script_balance.confirmed > 0 - && script_balance.unconfirmed == 0 - && swap.state != Refundable - { - // If there are unspent funds sent to the lockup script address then set - // the state to Refundable. - info!( - "Incoming Chain Swap {} has {} unspent sats. Setting the swap to refundable", - swap.id, script_balance.confirmed + "Error rescanning server lockup of incoming Chain Swap {}: {e:?}", + swap.id, ); - self.update_swap_info(&swap.id, Refundable, None, None, None, None) - .await?; - } else if script_balance.confirmed == 0 { - // If the funds sent to the lockup script address are spent then set the - // state back to Complete/Failed. - let to_state = match swap.claim_tx_id { - Some(_) => Complete, - None => Failed, - }; - - if to_state != swap.state { - info!( - "Incoming Chain Swap {} has 0 unspent sats. Setting the swap to {:?}", - swap.id, to_state - ); - self.update_swap_info(&swap.id, to_state, None, None, None, None) - .await?; - } } } Ok(()) } - pub(crate) async fn rescan_incoming_server_lockup_txs(&self) -> Result<()> { - let current_height = self.liquid_chain_service.lock().await.tip().await?; + async fn claim_outgoing(&self, height: u32) -> Result<()> { let chain_swaps: Vec = self .persister .list_chain_swaps()? .into_iter() .filter(|s| { - s.direction == Direction::Incoming && s.state == Pending && s.claim_tx_id.is_none() + s.direction == Direction::Outgoing && s.state == Pending && s.claim_tx_id.is_none() }) .collect(); info!( - "Rescanning {} incoming Chain Swap(s) server lockup txs at height {}", + "Rescanning {} outgoing Chain Swap(s) server lockup txs at height {}", chain_swaps.len(), - current_height + height ); for swap in chain_swaps { - if let Err(e) = self - .rescan_incoming_chain_swap_server_lockup_tx(&swap) - .await - { + if let Err(e) = self.claim_confirmed_server_lockup(&swap).await { error!( - "Error rescanning server lockup of incoming Chain Swap {}: {e:?}", + "Error rescanning server lockup of outgoing Chain Swap {}: {e:?}", swap.id ); } @@ -244,22 +167,25 @@ impl ChainSwapHandler { Ok(()) } - async fn rescan_incoming_chain_swap_server_lockup_tx(&self, swap: &ChainSwap) -> Result<()> { + async fn claim_confirmed_server_lockup(&self, swap: &ChainSwap) -> Result<()> { let Some(tx_id) = swap.server_lockup_tx_id.clone() else { // Skip the rescan if there is no server_lockup_tx_id yet return Ok(()); }; let swap_id = &swap.id; let swap_script = swap.get_claim_swap_script()?; - let script_history = self.fetch_liquid_script_history(&swap_script).await?; + let script_history = match swap.direction { + Direction::Incoming => self.fetch_liquid_script_history(&swap_script).await, + Direction::Outgoing => self.fetch_bitcoin_script_history(&swap_script).await, + }?; let tx_history = script_history .iter() .find(|h| h.txid.to_hex().eq(&tx_id)) .ok_or(anyhow!( - "Server lockup tx for incoming Chain Swap {swap_id} was not found, txid={tx_id}" + "Server lockup tx for Chain Swap {swap_id} was not found, txid={tx_id}" ))?; if tx_history.height > 0 { - info!("Incoming Chain Swap {swap_id} server lockup tx is confirmed"); + info!("Chain Swap {swap_id} server lockup tx is confirmed"); self.claim(swap_id) .await .map_err(|e| anyhow!("Could not claim Chain Swap {swap_id}: {e:?}"))?; @@ -267,56 +193,8 @@ impl ChainSwapHandler { Ok(()) } - pub(crate) async fn rescan_outgoing_claim_txs(&self) -> Result<()> { - let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32; - let chain_swaps: Vec = self - .persister - .list_chain_swaps()? - .into_iter() - .filter(|s| { - s.direction == Direction::Outgoing && s.state == Pending && s.claim_tx_id.is_some() - }) - .collect(); - info!( - "Rescanning {} outgoing Chain Swap(s) claim txs at height {}", - chain_swaps.len(), - current_height - ); - for swap in chain_swaps { - if let Err(e) = self.rescan_outgoing_chain_swap_claim_tx(&swap).await { - error!("Error rescanning outgoing Chain Swap {}: {e:?}", swap.id); - } - } - Ok(()) - } - - async fn rescan_outgoing_chain_swap_claim_tx(&self, swap: &ChainSwap) -> Result<()> { - if let Some(claim_address) = &swap.claim_address { - let address = Address::from_str(claim_address)?; - let claim_tx_id = swap.claim_tx_id.clone().ok_or(anyhow!("No claim tx id"))?; - let script_pubkey = address.assume_checked().script_pubkey(); - let script_history = self - .bitcoin_chain_service - .lock() - .await - .get_script_history(script_pubkey.as_script())?; - let claim_tx_history = script_history - .iter() - .find(|h| h.txid.to_hex().eq(&claim_tx_id) && h.height > 0); - if claim_tx_history.is_some() { - info!( - "Outgoing Chain Swap {} claim tx is confirmed. Setting the swap to Complete", - swap.id - ); - self.update_swap_info(&swap.id, Complete, None, None, None, None) - .await?; - } - } - Ok(()) - } - async fn on_new_incoming_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> { - let id = &update.id; + let id = update.id.clone(); let status = &update.status; let swap_state = ChainSwapStates::from_str(status) .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; @@ -329,11 +207,15 @@ impl ChainSwapHandler { if let Some(zero_conf_rejected) = update.zero_conf_rejected { info!("Is zero conf rejected for Chain Swap {id}: {zero_conf_rejected}"); self.persister - .update_chain_swap_accept_zero_conf(id, !zero_conf_rejected)?; + .update_chain_swap_accept_zero_conf(&id, !zero_conf_rejected)?; } if let Some(transaction) = update.transaction.clone() { - self.update_swap_info(id, Pending, None, Some(&transaction.id), None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: Pending, + user_lockup_tx_id: Some(transaction.id), + ..Default::default() + })?; } Ok(()) } @@ -362,11 +244,15 @@ impl ChainSwapHandler { } info!("Server lockup mempool transaction was verified for incoming Chain Swap {}", swap.id); - self.update_swap_info(id, Pending, Some(&transaction.id), None, None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id.clone(), + to_state: Pending, + server_lockup_tx_id: Some(transaction.id), + ..Default::default() + })?; if swap.accept_zero_conf { - self.claim(id).await.map_err(|e| { + self.claim(&id).await.map_err(|e| { error!("Could not cooperate Chain Swap {id} claim: {e}"); anyhow!("Could not post claim details. Err: {e:?}") })?; @@ -399,13 +285,17 @@ impl ChainSwapHandler { // Set the server_lockup_tx_id if it is verified or not. // If it is not yet confirmed, then it will be claimed after confirmation // in rescan_incoming_chain_swap_server_lockup_tx() - self.update_swap_info(id, Pending, Some(&transaction.id), None, None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id.clone(), + to_state: Pending, + server_lockup_tx_id: Some(transaction.id.clone()), + ..Default::default() + })?; match verify_res { Ok(_) => { info!("Server lockup transaction was verified for incoming Chain Swap {}", swap.id); - self.claim(id).await.map_err(|e| { + self.claim(&id).await.map_err(|e| { error!("Could not cooperate Chain Swap {id} claim: {e}"); anyhow!("Could not post claim details. Err: {e:?}") })?; @@ -456,13 +346,19 @@ impl ChainSwapHandler { match self.verify_user_lockup_tx(swap).await { Ok(_) => { info!("Chain Swap {id} user lockup tx was broadcast. Setting the swap to refundable."); - self.update_swap_info(id, Refundable, None, None, None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: Refundable, + ..Default::default() + })?; } Err(_) => { info!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed."); - self.update_swap_info(id, Failed, None, None, None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: Failed, + ..Default::default() + })?; } } } @@ -558,7 +454,7 @@ impl ChainSwapHandler { } async fn on_new_outgoing_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> { - let id = &update.id; + let id = update.id.clone(); let status = &update.status; let swap_state = ChainSwapStates::from_str(status) .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; @@ -575,7 +471,7 @@ impl ChainSwapHandler { // Create the user lockup tx (_, None) => { let create_response = swap.get_boltz_create_response()?; - let user_lockup_tx = self.lockup_funds(id, &create_response).await?; + let user_lockup_tx = self.lockup_funds(&id, &create_response).await?; let lockup_tx_id = user_lockup_tx.txid().to_string(); let lockup_tx_fees_sat: u64 = user_lockup_tx.all_fees().values().sum(); @@ -589,10 +485,14 @@ impl ChainSwapHandler { fees_sat: lockup_tx_fees_sat + swap.claim_fees_sat, payment_type: PaymentType::Send, is_confirmed: false, - }, None, None)?; + }, None, false)?; - self.update_swap_info(id, Pending, None, Some(&lockup_tx_id), None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: Pending, + user_lockup_tx_id: Some(lockup_tx_id), + ..Default::default() + })?; }, // Lockup tx already exists @@ -606,11 +506,15 @@ impl ChainSwapHandler { if let Some(zero_conf_rejected) = update.zero_conf_rejected { info!("Is zero conf rejected for Chain Swap {id}: {zero_conf_rejected}"); self.persister - .update_chain_swap_accept_zero_conf(id, !zero_conf_rejected)?; + .update_chain_swap_accept_zero_conf(&id, !zero_conf_rejected)?; } if let Some(transaction) = update.transaction.clone() { - self.update_swap_info(id, Pending, None, Some(&transaction.id), None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: Pending, + user_lockup_tx_id: Some(transaction.id), + ..Default::default() + })?; } Ok(()) } @@ -639,11 +543,15 @@ impl ChainSwapHandler { } info!("Server lockup mempool transaction was verified for outgoing Chain Swap {}", swap.id); - self.update_swap_info(id, Pending, Some(&transaction.id), None, None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id.clone(), + to_state: Pending, + server_lockup_tx_id: Some(transaction.id), + ..Default::default() + })?; if swap.accept_zero_conf { - self.claim(id).await.map_err(|e| { + self.claim(&id).await.map_err(|e| { error!("Could not cooperate Chain Swap {id} claim: {e}"); anyhow!("Could not post claim details. Err: {e:?}") })?; @@ -686,9 +594,13 @@ impl ChainSwapHandler { "Server lockup transaction was verified for outgoing Chain Swap {}", swap.id ); - self.update_swap_info(id, Pending, Some(&transaction.id), None, None, None) - .await?; - self.claim(id).await.map_err(|e| { + self.update_swap_info(&ChainSwapUpdate { + swap_id: id.clone(), + to_state: Pending, + server_lockup_tx_id: Some(transaction.id), + ..Default::default() + })?; + self.claim(&id).await.map_err(|e| { error!("Could not cooperate Chain Swap {id} claim: {e}"); anyhow!("Could not post claim details. Err: {e:?}") })?; @@ -729,20 +641,20 @@ impl ChainSwapHandler { // Set the payment state to `RefundPending`. This ensures that the // background thread will pick it up and try to refund it // periodically - self.update_swap_info( - &swap.id, - RefundPending, - None, - None, - None, - refund_tx_id.as_deref(), - ) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: RefundPending, + refund_tx_id, + ..Default::default() + })?; } None => { warn!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed."); - self.update_swap_info(id, Failed, None, None, None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: Failed, + ..Default::default() + })?; } } } @@ -795,52 +707,52 @@ impl ChainSwapHandler { Ok(lockup_tx) } - /// Transitions a Chain swap to a new state - pub(crate) async fn update_swap_info( - &self, - swap_id: &str, - to_state: PaymentState, - server_lockup_tx_id: Option<&str>, - user_lockup_tx_id: Option<&str>, - claim_tx_id: Option<&str>, - refund_tx_id: Option<&str>, - ) -> Result<(), PaymentError> { - info!("Transitioning Chain swap {swap_id} to {to_state:?} (server_lockup_tx_id = {:?}, user_lockup_tx_id = {:?}, claim_tx_id = {:?}), refund_tx_id = {:?})", server_lockup_tx_id, user_lockup_tx_id, claim_tx_id, refund_tx_id); - - let swap: ChainSwap = self - .persister + fn fetch_chain_swap_by_id(&self, swap_id: &str) -> Result { + self.persister .fetch_chain_swap_by_id(swap_id) .map_err(|_| PaymentError::PersistError)? .ok_or(PaymentError::Generic { err: format!("Chain Swap not found {swap_id}"), - })?; - let payment_id = match swap.direction { - Direction::Incoming => claim_tx_id.map(|c| c.to_string()).or(swap.claim_tx_id), - Direction::Outgoing => user_lockup_tx_id - .map(|c| c.to_string()) - .or(swap.user_lockup_tx_id), - }; + }) + } - Self::validate_state_transition(swap.state, to_state)?; - self.persister.try_handle_chain_swap_update( - swap_id, - to_state, - server_lockup_tx_id, - user_lockup_tx_id, - claim_tx_id, - refund_tx_id, - )?; - if let Some(payment_id) = payment_id { - let _ = self.subscription_notifier.send(payment_id); + // Updates the swap without state transition validation + pub(crate) fn update_swap(&self, updated_swap: ChainSwap) -> Result<(), PaymentError> { + let swap = self.fetch_chain_swap_by_id(&updated_swap.id)?; + if updated_swap != swap { + info!( + "Updating Chain swap {} to {:?} (user_lockup_tx_id = {:?}, server_lockup_tx_id = {:?}, claim_tx_id = {:?}, refund_tx_id = {:?})", + updated_swap.id, + updated_swap.state, + updated_swap.user_lockup_tx_id, + updated_swap.server_lockup_tx_id, + updated_swap.claim_tx_id, + updated_swap.refund_tx_id + ); + self.persister.insert_or_update_chain_swap(&updated_swap)?; + let _ = self.subscription_notifier.send(updated_swap.id); + } + Ok(()) + } + + // Updates the swap state with validation + pub(crate) fn update_swap_info( + &self, + swap_update: &ChainSwapUpdate, + ) -> Result<(), PaymentError> { + info!("Updating Chain swap {swap_update:?}"); + let swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?; + Self::validate_state_transition(swap.state, swap_update.to_state)?; + self.persister.try_handle_chain_swap_update(swap_update)?; + let updated_swap = self.fetch_chain_swap_by_id(&swap_update.swap_id)?; + if updated_swap != swap { + let _ = self.subscription_notifier.send(updated_swap.id); } Ok(()) } async fn claim(&self, swap_id: &str) -> Result<(), PaymentError> { - let swap = self - .persister - .fetch_chain_swap_by_id(swap_id)? - .ok_or(anyhow!("No Chain Swap found for ID {swap_id}"))?; + let swap = self.fetch_chain_swap_by_id(swap_id)?; ensure_sdk!(swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed); debug!("Initiating claim for Chain Swap {swap_id}"); @@ -893,33 +805,34 @@ impl ChainSwapHandler { match broadcast_res { Ok(claim_tx_id) => { - if swap.direction == Direction::Incoming { - // We insert a pseudo-claim-tx in case LWK fails to pick up the new mempool tx for a while - // This makes the tx known to the SDK (get_info, list_payments) instantly - self.persister.insert_or_update_payment( - PaymentTxData { - tx_id: claim_tx_id.clone(), - timestamp: Some(utils::now()), - amount_sat: swap.receiver_amount_sat, - fees_sat: 0, - payment_type: PaymentType::Receive, - is_confirmed: false, - }, - None, - None, - )?; - } + let payment_id = match swap.direction { + Direction::Incoming => { + // We insert a pseudo-claim-tx in case LWK fails to pick up the new mempool tx for a while + // This makes the tx known to the SDK (get_info, list_payments) instantly + self.persister.insert_or_update_payment( + PaymentTxData { + tx_id: claim_tx_id.clone(), + timestamp: Some(utils::now()), + amount_sat: swap.receiver_amount_sat, + fees_sat: 0, + payment_type: PaymentType::Receive, + is_confirmed: false, + }, + None, + false, + )?; + Some(claim_tx_id.clone()) + } + Direction::Outgoing => swap.user_lockup_tx_id, + }; info!("Successfully broadcast claim tx {claim_tx_id} for Chain Swap {swap_id}"); - self.update_swap_info( - &swap.id, - Pending, - None, - None, - Some(&claim_tx_id), - None, - ) - .await + // The claim_tx_id is already set by set_chain_swap_claim_tx_id. Manually trigger notifying + // subscribers as update_swap_info will not recognise a change to the swap + payment_id.and_then(|payment_id| { + self.subscription_notifier.send(payment_id).ok() + }); + Ok(()) } Err(err) => { // Multiple attempts to broadcast have failed. Unset the swap claim_tx_id @@ -993,13 +906,6 @@ impl ChainSwapHandler { } ); - ensure_sdk!( - swap.refund_tx_id.is_none(), - PaymentError::Generic { - err: format!("A refund tx for incoming Chain Swap {id} was already broadcast",) - } - ); - info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}",); let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else { @@ -1034,15 +940,12 @@ impl ChainSwapHandler { // After refund tx is broadcasted, set the payment state to `RefundPending`. This ensures: // - the swap is not shown in `list-refundables` anymore // - the background thread will move it to Failed once the refund tx confirms - self.update_swap_info( - &swap.id, - RefundPending, - None, - None, - None, - Some(&refund_tx_id), - ) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: swap.id, + to_state: RefundPending, + refund_tx_id: Some(refund_tx_id.clone()), + ..Default::default() + })?; Ok(refund_tx_id) } @@ -1110,111 +1013,56 @@ impl ChainSwapHandler { Ok(refund_tx_id) } - async fn check_swap_expiry(&self, swap: &ChainSwap) -> Result { - let swap_creation_time = UNIX_EPOCH + Duration::from_secs(swap.created_at as u64); - let duration_since_creation_time = SystemTime::now().duration_since(swap_creation_time)?; - if duration_since_creation_time.as_secs() < 60 * 10 { - return Ok(false); - } - - match swap.direction { - Direction::Incoming => { - let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?; - let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32; - let locktime_from_height = - LockTime::from_height(current_height).map_err(|e| PaymentError::Generic { - err: format!("Error getting locktime from height {current_height:?}: {e}",), - })?; - - info!("Checking Chain Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap.id, swap_script.locktime); - Ok(swap_script.locktime.is_implied_by(locktime_from_height)) - } - Direction::Outgoing => { - let swap_script = swap.get_lockup_swap_script()?.as_liquid_script()?; - let current_height = self.liquid_chain_service.lock().await.tip().await?; - let locktime_from_height = ElementsLockTime::from_height(current_height)?; - - info!("Checking Chain Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap.id, swap_script.locktime); - Ok(utils::is_locktime_expired( - locktime_from_height, - swap_script.locktime, - )) - } - } - } - - pub(crate) async fn track_refunds_and_refundables(&self) -> Result<(), PaymentError> { - let pending_swaps = self.persister.list_pending_chain_swaps()?; + async fn refund_outgoing(&self, height: u32) -> Result<(), PaymentError> { + // Get all pending outgoing chain swaps with no refund tx + let pending_swaps: Vec = self + .persister + .list_pending_chain_swaps()? + .into_iter() + .filter(|s| s.direction == Direction::Outgoing && s.refund_tx_id.is_none()) + .collect(); for swap in pending_swaps { - if swap.refund_tx_id.is_some() { - continue; - } - - let has_swap_expired = self.check_swap_expiry(&swap).await.unwrap_or(false); - - if !has_swap_expired && swap.state == Pending { - continue; - } - - match swap.direction { - // Track refunds - Direction::Outgoing => { - let refund_tx_id_result: Result = match swap.state { - Pending => self.refund_outgoing_swap(&swap, false).await, - RefundPending => match has_swap_expired { - true => { - self.refund_outgoing_swap(&swap, true) - .or_else(|e| { - warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}"); - self.refund_outgoing_swap(&swap, false) - }) - .await - } - false => self.refund_outgoing_swap(&swap, true).await, - }, - _ => { - continue; + let swap_script = swap.get_lockup_swap_script()?.as_liquid_script()?; + let locktime_from_height = ElementsLockTime::from_height(height) + .map_err(|e| PaymentError::Generic { err: e.to_string() })?; + info!("Checking Chain Swap {} expiration: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap.id, swap_script.locktime); + let has_swap_expired = + utils::is_locktime_expired(locktime_from_height, swap_script.locktime); + if has_swap_expired || swap.state == RefundPending { + let refund_tx_id_res = match swap.state { + Pending => self.refund_outgoing_swap(&swap, false).await, + RefundPending => match has_swap_expired { + true => { + self.refund_outgoing_swap(&swap, true) + .or_else(|e| { + warn!("Failed to initiate cooperative refund, switching to non-cooperative: {e:?}"); + self.refund_outgoing_swap(&swap, false) + }) + .await } - }; - - if let Ok(refund_tx_id) = refund_tx_id_result { - let update_swap_info_result = self - .update_swap_info( - &swap.id, - RefundPending, - None, - None, - None, - Some(&refund_tx_id), - ) - .await; - if let Err(err) = update_swap_info_result { - warn!( - "Could not update outgoing Chain swap {} information, error: {err:?}", - swap.id - ); - }; + false => self.refund_outgoing_swap(&swap, true).await, + }, + _ => { + continue; } - } + }; - // Track refundables by verifying that the expiry has elapsed, and set the state of the incoming swap to `Refundable` - Direction::Incoming => { - if swap.user_lockup_tx_id.is_some() && has_swap_expired { - let update_swap_info_result = self - .update_swap_info(&swap.id, Refundable, None, None, None, None) - .await; - - if let Err(err) = update_swap_info_result { - warn!( - "Could not update Chain swap {} information, error: {err:?}", - swap.id - ); - } - } + if let Ok(refund_tx_id) = refund_tx_id_res { + let update_swap_info_res = self.update_swap_info(&ChainSwapUpdate { + swap_id: swap.id.clone(), + to_state: RefundPending, + refund_tx_id: Some(refund_tx_id), + ..Default::default() + }); + if let Err(err) = update_swap_info_res { + warn!( + "Could not update outgoing Chain swap {} information: {err:?}", + swap.id + ); + }; } } } - Ok(()) } @@ -1402,8 +1250,12 @@ impl ChainSwapHandler { .ok_or(anyhow!("Script history has no transactions"))? .txid .to_hex(); - self.update_swap_info(&chain_swap.id, Pending, None, Some(&txid), None, None) - .await?; + self.update_swap_info(&ChainSwapUpdate { + swap_id: chain_swap.id.clone(), + to_state: Pending, + user_lockup_tx_id: Some(txid.clone()), + ..Default::default() + })?; Ok(txid) } } @@ -1447,30 +1299,25 @@ impl ChainSwapHandler { #[cfg(test)] mod tests { - use std::{ - collections::{HashMap, HashSet}, - sync::Arc, - }; - use anyhow::Result; + use std::collections::{HashMap, HashSet}; use crate::{ model::{ - Direction, + ChainSwapUpdate, Direction, PaymentState::{self, *}, }, test_utils::{ chain_swap::{new_chain_swap, new_chain_swap_handler}, - persist::new_persister, + persist::create_persister, }, }; #[tokio::test] async fn test_chain_swap_state_transitions() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; - let storage = Arc::new(storage); + create_persister!(persister); - let chain_swap_handler = new_chain_swap_handler(storage.clone())?; + let chain_swap_handler = new_chain_swap_handler(persister.clone())?; // Test valid combinations of states let all_states = HashSet::from([Created, Pending, Complete, TimedOut, Failed]); @@ -1494,11 +1341,14 @@ mod tests { for allowed_state in allowed_states { let chain_swap = new_chain_swap(Direction::Incoming, Some(*first_state), false, None); - storage.insert_chain_swap(&chain_swap)?; + persister.insert_or_update_chain_swap(&chain_swap)?; assert!(chain_swap_handler - .update_swap_info(&chain_swap.id, *allowed_state, None, None, None, None) - .await + .update_swap_info(&ChainSwapUpdate { + swap_id: chain_swap.id, + to_state: *allowed_state, + ..Default::default() + }) .is_ok()); } } @@ -1518,11 +1368,14 @@ mod tests { for disallowed_state in disallowed_states { let chain_swap = new_chain_swap(Direction::Incoming, Some(*first_state), false, None); - storage.insert_chain_swap(&chain_swap)?; + persister.insert_or_update_chain_swap(&chain_swap)?; assert!(chain_swap_handler - .update_swap_info(&chain_swap.id, *disallowed_state, None, None, None, None) - .await + .update_swap_info(&ChainSwapUpdate { + swap_id: chain_swap.id, + to_state: *disallowed_state, + ..Default::default() + }) .is_err()); } } diff --git a/lib/core/src/event.rs b/lib/core/src/event.rs index 4ddb6b9e8..a80eb4ffe 100644 --- a/lib/core/src/event.rs +++ b/lib/core/src/event.rs @@ -3,7 +3,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; -use log::info; +use log::{debug, info}; use tokio::sync::{broadcast, RwLock}; use crate::model::{EventListener, SdkEvent}; @@ -42,6 +42,7 @@ impl EventManager { match self.is_paused.load(Ordering::SeqCst) { true => info!("Event notifications are paused, not emitting event {e:?}"), false => { + debug!("Emitting event: {e:?}"); let _ = self.notifier.send(e.clone()); for listener in (*self.listeners.read().await).values() { diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index 0c34a9f73..f2d83df17 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -2372,6 +2372,7 @@ impl SseDecode for crate::model::Config { let mut var_network = ::sse_decode(deserializer); let mut var_paymentTimeoutSec = ::sse_decode(deserializer); let mut var_zeroConfMinFeeRateMsat = ::sse_decode(deserializer); + let mut var_syncServiceUrl = ::sse_decode(deserializer); let mut var_zeroConfMaxAmountSat = >::sse_decode(deserializer); let mut var_breezApiKey = >::sse_decode(deserializer); let mut var_externalInputParsers = @@ -2386,6 +2387,7 @@ impl SseDecode for crate::model::Config { network: var_network, payment_timeout_sec: var_paymentTimeoutSec, zero_conf_min_fee_rate_msat: var_zeroConfMinFeeRateMsat, + sync_service_url: var_syncServiceUrl, zero_conf_max_amount_sat: var_zeroConfMaxAmountSat, breez_api_key: var_breezApiKey, external_input_parsers: var_externalInputParsers, @@ -2746,6 +2748,18 @@ impl SseDecode for crate::model::ListPaymentDetails { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -2762,6 +2776,7 @@ impl SseDecode for crate::model::ListPaymentsRequest { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut var_filters = >>::sse_decode(deserializer); + let mut var_states = >>::sse_decode(deserializer); let mut var_fromTimestamp = >::sse_decode(deserializer); let mut var_toTimestamp = >::sse_decode(deserializer); let mut var_offset = >::sse_decode(deserializer); @@ -2769,6 +2784,7 @@ impl SseDecode for crate::model::ListPaymentsRequest { let mut var_details = >::sse_decode(deserializer); return crate::model::ListPaymentsRequest { filters: var_filters, + states: var_states, from_timestamp: var_fromTimestamp, to_timestamp: var_toTimestamp, offset: var_offset, @@ -2975,6 +2991,30 @@ impl SseDecode for crate::bindings::LnUrlErrorData { } } +impl SseDecode for crate::model::LnUrlInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_lnAddress = >::sse_decode(deserializer); + let mut var_lnurlPayComment = >::sse_decode(deserializer); + let mut var_lnurlPayDomain = >::sse_decode(deserializer); + let mut var_lnurlPayMetadata = >::sse_decode(deserializer); + let mut var_lnurlPaySuccessAction = + >::sse_decode(deserializer); + let mut var_lnurlPayUnprocessedSuccessAction = + >::sse_decode(deserializer); + let mut var_lnurlWithdrawEndpoint = >::sse_decode(deserializer); + return crate::model::LnUrlInfo { + ln_address: var_lnAddress, + lnurl_pay_comment: var_lnurlPayComment, + lnurl_pay_domain: var_lnurlPayDomain, + lnurl_pay_metadata: var_lnurlPayMetadata, + lnurl_pay_success_action: var_lnurlPaySuccessAction, + lnurl_pay_unprocessed_success_action: var_lnurlPayUnprocessedSuccessAction, + lnurl_withdraw_endpoint: var_lnurlWithdrawEndpoint, + }; + } +} + impl SseDecode for crate::bindings::duplicates::LnUrlPayError { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3386,6 +3426,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3478,6 +3529,17 @@ impl SseDecode for Option> { } } +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3561,6 +3623,7 @@ impl SseDecode for crate::model::PaymentDetails { let mut var_bolt11 = >::sse_decode(deserializer); let mut var_bolt12Offer = >::sse_decode(deserializer); let mut var_paymentHash = >::sse_decode(deserializer); + let mut var_lnurlInfo = >::sse_decode(deserializer); let mut var_refundTxId = >::sse_decode(deserializer); let mut var_refundTxAmountSat = >::sse_decode(deserializer); return crate::model::PaymentDetails::Lightning { @@ -3570,6 +3633,7 @@ impl SseDecode for crate::model::PaymentDetails { bolt11: var_bolt11, bolt12_offer: var_bolt12Offer, payment_hash: var_paymentHash, + lnurl_info: var_lnurlInfo, refund_tx_id: var_refundTxId, refund_tx_amount_sat: var_refundTxAmountSat, }; @@ -3779,11 +3843,15 @@ impl SseDecode for crate::model::PrepareLnUrlPayResponse { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut var_destination = ::sse_decode(deserializer); let mut var_feesSat = ::sse_decode(deserializer); + let mut var_data = ::sse_decode(deserializer); + let mut var_comment = >::sse_decode(deserializer); let mut var_successAction = >::sse_decode(deserializer); return crate::model::PrepareLnUrlPayResponse { destination: var_destination, fees_sat: var_feesSat, + data: var_data, + comment: var_comment, success_action: var_successAction, }; } @@ -4600,6 +4668,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::Config { self.zero_conf_min_fee_rate_msat .into_into_dart() .into_dart(), + self.sync_service_url.into_into_dart().into_dart(), self.zero_conf_max_amount_sat.into_into_dart().into_dart(), self.breez_api_key.into_into_dart().into_dart(), self.external_input_parsers.into_into_dart().into_dart(), @@ -4912,6 +4981,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::ListPaymentsRequest { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ self.filters.into_into_dart().into_dart(), + self.states.into_into_dart().into_dart(), self.from_timestamp.into_into_dart().into_dart(), self.to_timestamp.into_into_dart().into_dart(), self.offset.into_into_dart().into_dart(), @@ -5106,6 +5176,29 @@ impl flutter_rust_bridge::IntoIntoDart flutter_rust_bridge::for_generated::DartAbi { + [ + self.ln_address.into_into_dart().into_dart(), + self.lnurl_pay_comment.into_into_dart().into_dart(), + self.lnurl_pay_domain.into_into_dart().into_dart(), + self.lnurl_pay_metadata.into_into_dart().into_dart(), + self.lnurl_pay_success_action.into_into_dart().into_dart(), + self.lnurl_pay_unprocessed_success_action + .into_into_dart() + .into_dart(), + self.lnurl_withdraw_endpoint.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::model::LnUrlInfo {} +impl flutter_rust_bridge::IntoIntoDart for crate::model::LnUrlInfo { + fn into_into_dart(self) -> crate::model::LnUrlInfo { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::bindings::duplicates::LnUrlPayError { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -5598,6 +5691,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::PaymentDetails { bolt11, bolt12_offer, payment_hash, + lnurl_info, refund_tx_id, refund_tx_amount_sat, } => [ @@ -5608,6 +5702,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::PaymentDetails { bolt11.into_into_dart().into_dart(), bolt12_offer.into_into_dart().into_dart(), payment_hash.into_into_dart().into_dart(), + lnurl_info.into_into_dart().into_dart(), refund_tx_id.into_into_dart().into_dart(), refund_tx_amount_sat.into_into_dart().into_dart(), ] @@ -5838,6 +5933,8 @@ impl flutter_rust_bridge::IntoDart for crate::model::PrepareLnUrlPayResponse { [ self.destination.into_into_dart().into_dart(), self.fees_sat.into_into_dart().into_dart(), + self.data.into_into_dart().into_dart(), + self.comment.into_into_dart().into_dart(), self.success_action.into_into_dart().into_dart(), ] .into_dart() @@ -6679,6 +6776,7 @@ impl SseEncode for crate::model::Config { ::sse_encode(self.network, serializer); ::sse_encode(self.payment_timeout_sec, serializer); ::sse_encode(self.zero_conf_min_fee_rate_msat, serializer); + ::sse_encode(self.sync_service_url, serializer); >::sse_encode(self.zero_conf_max_amount_sat, serializer); >::sse_encode(self.breez_api_key, serializer); >>::sse_encode( @@ -6959,6 +7057,16 @@ impl SseEncode for crate::model::ListPaymentDetails { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6973,6 +7081,7 @@ impl SseEncode for crate::model::ListPaymentsRequest { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { >>::sse_encode(self.filters, serializer); + >>::sse_encode(self.states, serializer); >::sse_encode(self.from_timestamp, serializer); >::sse_encode(self.to_timestamp, serializer); >::sse_encode(self.offset, serializer); @@ -7128,6 +7237,25 @@ impl SseEncode for crate::bindings::LnUrlErrorData { } } +impl SseEncode for crate::model::LnUrlInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.ln_address, serializer); + >::sse_encode(self.lnurl_pay_comment, serializer); + >::sse_encode(self.lnurl_pay_domain, serializer); + >::sse_encode(self.lnurl_pay_metadata, serializer); + >::sse_encode( + self.lnurl_pay_success_action, + serializer, + ); + >::sse_encode( + self.lnurl_pay_unprocessed_success_action, + serializer, + ); + >::sse_encode(self.lnurl_withdraw_endpoint, serializer); + } +} + impl SseEncode for crate::bindings::duplicates::LnUrlPayError { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7457,6 +7585,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7537,6 +7675,16 @@ impl SseEncode for Option> { } } +impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7599,6 +7747,7 @@ impl SseEncode for crate::model::PaymentDetails { bolt11, bolt12_offer, payment_hash, + lnurl_info, refund_tx_id, refund_tx_amount_sat, } => { @@ -7609,6 +7758,7 @@ impl SseEncode for crate::model::PaymentDetails { >::sse_encode(bolt11, serializer); >::sse_encode(bolt12_offer, serializer); >::sse_encode(payment_hash, serializer); + >::sse_encode(lnurl_info, serializer); >::sse_encode(refund_tx_id, serializer); >::sse_encode(refund_tx_amount_sat, serializer); } @@ -7810,6 +7960,8 @@ impl SseEncode for crate::model::PrepareLnUrlPayResponse { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode(self.destination, serializer); ::sse_encode(self.fees_sat, serializer); + ::sse_encode(self.data, serializer); + >::sse_encode(self.comment, serializer); >::sse_encode(self.success_action, serializer); } } @@ -8526,6 +8678,13 @@ mod io { CstDecode::::cst_decode(*wrap).into() } } + impl CstDecode for *mut wire_cst_ln_url_info { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::model::LnUrlInfo { + let wrap = unsafe { flutter_rust_bridge::for_generated::box_from_leak_ptr(self) }; + CstDecode::::cst_decode(*wrap).into() + } + } impl CstDecode for *mut wire_cst_ln_url_pay_error_data { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::bindings::LnUrlPayErrorData { @@ -8777,6 +8936,7 @@ mod io { network: self.network.cst_decode(), payment_timeout_sec: self.payment_timeout_sec.cst_decode(), zero_conf_min_fee_rate_msat: self.zero_conf_min_fee_rate_msat.cst_decode(), + sync_service_url: self.sync_service_url.cst_decode(), zero_conf_max_amount_sat: self.zero_conf_max_amount_sat.cst_decode(), breez_api_key: self.breez_api_key.cst_decode(), external_input_parsers: self.external_input_parsers.cst_decode(), @@ -9050,6 +9210,16 @@ mod io { } } } + impl CstDecode> for *mut wire_cst_list_payment_state { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> Vec { + let vec = unsafe { + let wrap = flutter_rust_bridge::for_generated::box_from_leak_ptr(self); + flutter_rust_bridge::for_generated::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(CstDecode::cst_decode).collect() + } + } impl CstDecode> for *mut wire_cst_list_payment_type { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> Vec { @@ -9065,6 +9235,7 @@ mod io { fn cst_decode(self) -> crate::model::ListPaymentsRequest { crate::model::ListPaymentsRequest { filters: self.filters.cst_decode(), + states: self.states.cst_decode(), from_timestamp: self.from_timestamp.cst_decode(), to_timestamp: self.to_timestamp.cst_decode(), offset: self.offset.cst_decode(), @@ -9226,6 +9397,22 @@ mod io { } } } + impl CstDecode for wire_cst_ln_url_info { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::model::LnUrlInfo { + crate::model::LnUrlInfo { + ln_address: self.ln_address.cst_decode(), + lnurl_pay_comment: self.lnurl_pay_comment.cst_decode(), + lnurl_pay_domain: self.lnurl_pay_domain.cst_decode(), + lnurl_pay_metadata: self.lnurl_pay_metadata.cst_decode(), + lnurl_pay_success_action: self.lnurl_pay_success_action.cst_decode(), + lnurl_pay_unprocessed_success_action: self + .lnurl_pay_unprocessed_success_action + .cst_decode(), + lnurl_withdraw_endpoint: self.lnurl_withdraw_endpoint.cst_decode(), + } + } + } impl CstDecode for wire_cst_ln_url_pay_error { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::bindings::duplicates::LnUrlPayError { @@ -9575,6 +9762,7 @@ mod io { bolt11: ans.bolt11.cst_decode(), bolt12_offer: ans.bolt12_offer.cst_decode(), payment_hash: ans.payment_hash.cst_decode(), + lnurl_info: ans.lnurl_info.cst_decode(), refund_tx_id: ans.refund_tx_id.cst_decode(), refund_tx_amount_sat: ans.refund_tx_amount_sat.cst_decode(), } @@ -9715,6 +9903,8 @@ mod io { crate::model::PrepareLnUrlPayResponse { destination: self.destination.cst_decode(), fees_sat: self.fees_sat.cst_decode(), + data: self.data.cst_decode(), + comment: self.comment.cst_decode(), success_action: self.success_action.cst_decode(), } } @@ -10237,6 +10427,7 @@ mod io { network: Default::default(), payment_timeout_sec: Default::default(), zero_conf_min_fee_rate_msat: Default::default(), + sync_service_url: core::ptr::null_mut(), zero_conf_max_amount_sat: core::ptr::null_mut(), breez_api_key: core::ptr::null_mut(), external_input_parsers: core::ptr::null_mut(), @@ -10410,6 +10601,7 @@ mod io { fn new_with_null_ptr() -> Self { Self { filters: core::ptr::null_mut(), + states: core::ptr::null_mut(), from_timestamp: core::ptr::null_mut(), to_timestamp: core::ptr::null_mut(), offset: core::ptr::null_mut(), @@ -10530,6 +10722,24 @@ mod io { Self::new_with_null_ptr() } } + impl NewWithNullPtr for wire_cst_ln_url_info { + fn new_with_null_ptr() -> Self { + Self { + ln_address: core::ptr::null_mut(), + lnurl_pay_comment: core::ptr::null_mut(), + lnurl_pay_domain: core::ptr::null_mut(), + lnurl_pay_metadata: core::ptr::null_mut(), + lnurl_pay_success_action: core::ptr::null_mut(), + lnurl_pay_unprocessed_success_action: core::ptr::null_mut(), + lnurl_withdraw_endpoint: core::ptr::null_mut(), + } + } + } + impl Default for wire_cst_ln_url_info { + fn default() -> Self { + Self::new_with_null_ptr() + } + } impl NewWithNullPtr for wire_cst_ln_url_pay_error { fn new_with_null_ptr() -> Self { Self { @@ -10866,6 +11076,8 @@ mod io { Self { destination: Default::default(), fees_sat: Default::default(), + data: Default::default(), + comment: core::ptr::null_mut(), success_action: core::ptr::null_mut(), } } @@ -11775,6 +11987,14 @@ mod io { ) } + #[no_mangle] + pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info( + ) -> *mut wire_cst_ln_url_info { + flutter_rust_bridge::for_generated::new_leak_box_ptr( + wire_cst_ln_url_info::new_with_null_ptr(), + ) + } + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_error_data( ) -> *mut wire_cst_ln_url_pay_error_data { @@ -12091,6 +12311,17 @@ mod io { flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) } + #[no_mangle] + pub extern "C" fn frbgen_breez_liquid_cst_new_list_payment_state( + len: i32, + ) -> *mut wire_cst_list_payment_state { + let wrap = wire_cst_list_payment_state { + ptr: flutter_rust_bridge::for_generated::new_leak_vec_ptr(Default::default(), len), + len, + }; + flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) + } + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_cst_new_list_payment_type( len: i32, @@ -12275,6 +12506,7 @@ mod io { network: i32, payment_timeout_sec: u64, zero_conf_min_fee_rate_msat: u32, + sync_service_url: *mut wire_cst_list_prim_u_8_strict, zero_conf_max_amount_sat: *mut u64, breez_api_key: *mut wire_cst_list_prim_u_8_strict, external_input_parsers: *mut wire_cst_list_external_input_parser, @@ -12497,6 +12729,12 @@ mod io { } #[repr(C)] #[derive(Clone, Copy)] + pub struct wire_cst_list_payment_state { + ptr: *mut i32, + len: i32, + } + #[repr(C)] + #[derive(Clone, Copy)] pub struct wire_cst_list_payment_type { ptr: *mut i32, len: i32, @@ -12505,6 +12743,7 @@ mod io { #[derive(Clone, Copy)] pub struct wire_cst_list_payments_request { filters: *mut wire_cst_list_payment_type, + states: *mut wire_cst_list_payment_state, from_timestamp: *mut i64, to_timestamp: *mut i64, offset: *mut u32, @@ -12635,6 +12874,17 @@ mod io { } #[repr(C)] #[derive(Clone, Copy)] + pub struct wire_cst_ln_url_info { + ln_address: *mut wire_cst_list_prim_u_8_strict, + lnurl_pay_comment: *mut wire_cst_list_prim_u_8_strict, + lnurl_pay_domain: *mut wire_cst_list_prim_u_8_strict, + lnurl_pay_metadata: *mut wire_cst_list_prim_u_8_strict, + lnurl_pay_success_action: *mut wire_cst_success_action_processed, + lnurl_pay_unprocessed_success_action: *mut wire_cst_success_action, + lnurl_withdraw_endpoint: *mut wire_cst_list_prim_u_8_strict, + } + #[repr(C)] + #[derive(Clone, Copy)] pub struct wire_cst_ln_url_pay_error { tag: i32, kind: LnUrlPayErrorKind, @@ -12955,6 +13205,7 @@ mod io { bolt11: *mut wire_cst_list_prim_u_8_strict, bolt12_offer: *mut wire_cst_list_prim_u_8_strict, payment_hash: *mut wire_cst_list_prim_u_8_strict, + lnurl_info: *mut wire_cst_ln_url_info, refund_tx_id: *mut wire_cst_list_prim_u_8_strict, refund_tx_amount_sat: *mut u64, } @@ -13070,6 +13321,8 @@ mod io { pub struct wire_cst_prepare_ln_url_pay_response { destination: wire_cst_send_destination, fees_sat: u64, + data: wire_cst_ln_url_pay_request_data, + comment: *mut wire_cst_list_prim_u_8_strict, success_action: *mut wire_cst_success_action, } #[repr(C)] diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index ddaf07f49..7e00a4f9d 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -176,12 +176,12 @@ pub mod logger; pub mod model; pub mod persist; pub mod receive_swap; -#[allow(dead_code)] -mod restore; +pub(crate) mod recover; pub mod sdk; pub(crate) mod send_swap; pub(crate) mod signer; pub(crate) mod swapper; +pub(crate) mod sync; pub(crate) mod test_utils; pub(crate) mod utils; pub(crate) mod wallet; diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 65023ddac..e24508f6b 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; use anyhow::{anyhow, Result}; - -use boltz_client::boltz::ChainPair; +use async_trait::async_trait; use boltz_client::{ bitcoin::ScriptBuf, + boltz::ChainPair, network::Chain, swaps::boltz::{ CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree, @@ -30,6 +30,7 @@ use crate::utils; // Both use f64 for the maximum precision when converting between units pub const STANDARD_FEE_RATE_SAT_PER_VBYTE: f64 = 0.1; pub const LOWBALL_FEE_RATE_SAT_PER_VBYTE: f64 = 0.01; +const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology"; /// Configuration for the Liquid SDK #[derive(Clone, Debug, Serialize)] @@ -49,6 +50,8 @@ pub struct Config { pub payment_timeout_sec: u64, /// Zero-conf minimum accepted fee-rate in millisatoshis per vbyte pub zero_conf_min_fee_rate_msat: u32, + /// The url of the real-time sync service + pub sync_service_url: String, /// Maximum amount in satoshi to accept zero-conf payments with /// Defaults to [DEFAULT_ZERO_CONF_MAX_SAT] pub zero_conf_max_amount_sat: Option, @@ -75,6 +78,7 @@ impl Config { network: LiquidNetwork::Mainnet, payment_timeout_sec: 15, zero_conf_min_fee_rate_msat: DEFAULT_ZERO_CONF_MIN_FEE_RATE_MAINNET, + sync_service_url: BREEZ_SYNC_SERVICE_URL.to_string(), zero_conf_max_amount_sat: None, breez_api_key: Some(breez_api_key), external_input_parsers: None, @@ -92,6 +96,7 @@ impl Config { network: LiquidNetwork::Testnet, payment_timeout_sec: 15, zero_conf_min_fee_rate_msat: DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET, + sync_service_url: BREEZ_SYNC_SERVICE_URL.to_string(), zero_conf_max_amount_sat: None, breez_api_key, external_input_parsers: None, @@ -289,6 +294,12 @@ pub trait Signer: Send + Sync { /// HMAC-SHA256 using the private key derived from the given derivation path /// This is used to calculate the linking key of lnurl-auth specification: fn hmac_sha256(&self, msg: Vec, derivation_path: String) -> Result, SignerError>; + + /// Encrypts a message using (ECIES)[ecies::encrypt] + fn ecies_encrypt(&self, msg: Vec) -> Result, SignerError>; + + /// Decrypts a message using (ECIES)[ecies::decrypt] + fn ecies_decrypt(&self, msg: Vec) -> Result, SignerError>; } /// An argument when calling [crate::sdk::LiquidSdk::connect]. @@ -519,7 +530,7 @@ pub struct RefundResponse { } /// Returned when calling [crate::sdk::LiquidSdk::get_info]. -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct GetInfoResponse { /// Usable balance. This is the confirmed onchain balance minus `pending_send_sat`. pub balance_sat: u64, @@ -584,6 +595,7 @@ pub struct RestoreRequest { #[derive(Default)] pub struct ListPaymentsRequest { pub filters: Option>, + pub states: Option>, /// Epoch time, in seconds pub from_timestamp: Option, /// Epoch time, in seconds @@ -610,6 +622,13 @@ pub enum GetPaymentRequest { Lightning { payment_hash: String }, } +/// Trait that can be used to react to new blocks from Bitcoin and Liquid chains +#[async_trait] +pub(crate) trait BlockListener: Send + Sync { + async fn on_bitcoin_block(&self, height: u32); + async fn on_liquid_block(&self, height: u32); +} + // A swap enum variant #[derive(Clone, Debug)] pub(crate) enum Swap { @@ -626,6 +645,21 @@ impl Swap { } } } +impl From for Swap { + fn from(swap: ChainSwap) -> Self { + Self::Chain(swap) + } +} +impl From for Swap { + fn from(swap: SendSwap) -> Self { + Self::Send(swap) + } +} +impl From for Swap { + fn from(swap: ReceiveSwap) -> Self { + Self::Receive(swap) + } +} #[derive(Clone, Debug)] pub(crate) enum SwapScriptV2 { @@ -648,7 +682,7 @@ impl SwapScriptV2 { } } -#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum Direction { Incoming = 0, Outgoing = 1, @@ -674,7 +708,7 @@ impl FromSql for Direction { /// A chain swap /// /// See -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct ChainSwap { pub(crate) id: String, pub(crate) direction: Direction, @@ -812,8 +846,19 @@ impl ChainSwap { } } +#[derive(Clone, Debug, Default)] +pub(crate) struct ChainSwapUpdate { + pub(crate) swap_id: String, + pub(crate) to_state: PaymentState, + pub(crate) server_lockup_tx_id: Option, + pub(crate) user_lockup_tx_id: Option, + pub(crate) claim_address: Option, + pub(crate) claim_tx_id: Option, + pub(crate) refund_tx_id: Option, +} + /// A submarine swap, used for Send -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct SendSwap { pub(crate) id: String, /// Bolt11 or Bolt12 invoice. This is determined by whether `bolt12_offer` is set or not. @@ -900,7 +945,7 @@ impl SendSwap { } /// A reverse swap, used for Receive -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct ReceiveSwap { pub(crate) id: String, pub(crate) preimage: String, @@ -918,12 +963,10 @@ pub(crate) struct ReceiveSwap { pub(crate) claim_fees_sat: u64, /// Persisted as soon as a claim tx is broadcast pub(crate) claim_tx_id: Option, - /// Persisted only when the lockup tx is broadcast + /// The transaction id of the swapper's tx broadcast pub(crate) lockup_tx_id: Option, /// The address reserved for a magic routing hint payment pub(crate) mrh_address: String, - /// The script pubkey for a magic routing hint payment - pub(crate) mrh_script_pubkey: String, /// Persisted only if a transaction is sent to the `mrh_address` pub(crate) mrh_tx_id: Option, /// Until the lockup tx is seen in the mempool, it contains the swap creation time. @@ -1011,8 +1054,10 @@ pub struct RefundableSwap { } /// The payment state of an individual payment. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Hash)] +#[derive(Clone, Copy, Debug, Default, EnumString, Eq, PartialEq, Serialize, Hash)] +#[strum(serialize_all = "lowercase")] pub enum PaymentState { + #[default] Created = 0, /// ## Receive Swaps @@ -1220,8 +1265,21 @@ pub struct PaymentSwapData { pub status: PaymentState, } +/// Represents the payment LNURL info +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct LnUrlInfo { + pub ln_address: Option, + pub lnurl_pay_comment: Option, + pub lnurl_pay_domain: Option, + pub lnurl_pay_metadata: Option, + pub lnurl_pay_success_action: Option, + pub lnurl_pay_unprocessed_success_action: Option, + pub lnurl_withdraw_endpoint: Option, +} + /// The specific details of a payment, depending on its type #[derive(Debug, Clone, PartialEq, Serialize)] +#[allow(clippy::large_enum_variant)] pub enum PaymentDetails { /// Swapping to or from Lightning Lightning { @@ -1243,6 +1301,9 @@ pub enum PaymentDetails { /// The payment hash of the invoice payment_hash: Option, + /// The payment LNURL info + lnurl_info: Option, + /// For a Send swap which was refunded, this is the refund tx id refund_tx_id: Option, @@ -1354,7 +1415,11 @@ pub struct Payment { pub details: PaymentDetails, } impl Payment { - pub(crate) fn from_pending_swap(swap: PaymentSwapData, payment_type: PaymentType) -> Payment { + pub(crate) fn from_pending_swap( + swap: PaymentSwapData, + payment_type: PaymentType, + payment_details: PaymentDetails, + ) -> Payment { let amount_sat = match payment_type { PaymentType::Receive => swap.receiver_amount_sat, PaymentType::Send => swap.payer_amount_sat, @@ -1369,16 +1434,7 @@ impl Payment { swapper_fees_sat: Some(swap.swapper_fees_sat), payment_type, status: swap.status, - details: PaymentDetails::Lightning { - swap_id: swap.swap_id, - preimage: swap.preimage, - bolt11: swap.bolt11, - bolt12_offer: swap.bolt12_offer, - payment_hash: swap.payment_hash, - description: swap.description, - refund_tx_id: swap.refund_tx_id, - refund_tx_amount_sat: swap.refund_tx_amount_sat, - }, + details: payment_details, } } @@ -1565,6 +1621,10 @@ pub struct PrepareLnUrlPayResponse { pub destination: SendDestination, /// The fees in satoshis to send the payment pub fees_sat: u64, + /// The [LnUrlPayRequestData] returned by [crate::input_parser::parse] + pub data: LnUrlPayRequestData, + /// An optional comment for this payment + pub comment: Option, /// The unprocessed LUD-09 success action. This will be processed and decrypted if /// needed after calling [crate::sdk::LiquidSdk::lnurl_pay] pub success_action: Option, @@ -1687,3 +1747,19 @@ macro_rules! get_invoice_description { } }; } + +#[macro_export] +macro_rules! get_updated_fields { + ($($var:ident),* $(,)?) => {{ + let mut options = Vec::new(); + $( + if $var.is_some() { + options.push(stringify!($var).to_string()); + } + )* + match options.len() > 0 { + true => Some(options), + false => None, + } + }}; +} diff --git a/lib/core/src/persist/address.rs b/lib/core/src/persist/address.rs index 4e66b51d1..19512e445 100644 --- a/lib/core/src/persist/address.rs +++ b/lib/core/src/persist/address.rs @@ -1,6 +1,6 @@ use anyhow::Result; use log::debug; -use rusqlite::{Row, Transaction, TransactionBehavior}; +use rusqlite::{Connection, Row, TransactionBehavior}; use crate::error::PaymentError; @@ -80,7 +80,10 @@ impl Persister { Ok(()) } - fn delete_reserved_address_inner(tx: &Transaction, address: &str) -> Result<(), PaymentError> { + pub(crate) fn delete_reserved_address_inner( + tx: &Connection, + address: &str, + ) -> Result<(), PaymentError> { tx.execute( "DELETE FROM reserved_addresses WHERE address = ?", [address], @@ -101,11 +104,11 @@ impl Persister { mod tests { use anyhow::Result; - use crate::test_utils::persist::new_persister; + use crate::test_utils::persist::create_persister; #[test] fn test_next_expired_reserved_address() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let address = "tlq1pq2amlulhea6ltq7x3eu9atsc2nnrer7yt7xve363zxedqwu2mk6ctcyv9awl8xf28cythreqklt5q0qqwsxzlm6wu4z6d574adl9zh2zmr0h85gt534n"; storage.insert_or_update_reserved_address(address, 100)?; @@ -131,7 +134,7 @@ mod tests { #[test] fn test_delete_reserved_address() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let address = "tlq1pq2amlulhea6ltq7x3eu9atsc2nnrer7yt7xve363zxedqwu2mk6ctcyv9awl8xf28cythreqklt5q0qqwsxzlm6wu4z6d574adl9zh2zmr0h85gt534n"; storage.insert_or_update_reserved_address(address, 100)?; diff --git a/lib/core/src/persist/backup.rs b/lib/core/src/persist/backup.rs index 344567900..1d34f1b56 100644 --- a/lib/core/src/persist/backup.rs +++ b/lib/core/src/persist/backup.rs @@ -40,22 +40,22 @@ mod tests { use crate::{ model::PaymentState, - test_utils::persist::{new_persister, new_receive_swap, new_send_swap}, + test_utils::persist::{create_persister, new_receive_swap, new_send_swap}, }; #[test] fn test_backup_and_restore() -> Result<()> { - let (_local_temp_dir, local) = new_persister()?; + create_persister!(local); - local.insert_send_swap(&new_send_swap(Some(PaymentState::Pending)))?; - local.insert_receive_swap(&new_receive_swap(Some(PaymentState::Pending)))?; + local.insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending)))?; + local.insert_or_update_receive_swap(&new_receive_swap(Some(PaymentState::Pending)))?; assert_eq!(local.list_ongoing_swaps()?.len(), 2); let backup_path = local.get_default_backup_path(); local.backup(backup_path.clone())?; assert!(backup_path.exists()); - let (_remote_temp_dir, remote) = new_persister()?; + create_persister!(remote); remote.restore_from_backup(backup_path)?; assert_eq!(remote.list_ongoing_swaps()?.len(), 2); diff --git a/lib/core/src/persist/cache.rs b/lib/core/src/persist/cache.rs index 085cf334b..6f04e40ec 100644 --- a/lib/core/src/persist/cache.rs +++ b/lib/core/src/persist/cache.rs @@ -2,13 +2,16 @@ use anyhow::Result; use rusqlite::{Transaction, TransactionBehavior}; use std::str::FromStr; +use crate::model::GetInfoResponse; +use crate::sync::model::{data::LAST_DERIVATION_INDEX_DATA_ID, RecordType}; + use super::Persister; +const KEY_WALLET_INFO: &str = "wallet_info"; const KEY_SWAPPER_PROXY_URL: &str = "swapper_proxy_url"; const KEY_IS_FIRST_SYNC_COMPLETE: &str = "is_first_sync_complete"; const KEY_WEBHOOK_URL: &str = "webhook_url"; -// TODO: The `last_derivation_index` needs to be synced -const KEY_LAST_DERIVATION_INDEX: &str = "last_derivation_index"; +pub(crate) const KEY_LAST_DERIVATION_INDEX: &str = "last_derivation_index"; impl Persister { fn get_cached_item_inner(tx: &Transaction, key: &str) -> Result> { @@ -20,7 +23,11 @@ impl Persister { Ok(res.ok()) } - fn update_cached_item_inner(tx: &Transaction, key: &str, value: String) -> Result<()> { + pub(crate) fn update_cached_item_inner( + tx: &Transaction, + key: &str, + value: String, + ) -> Result<()> { tx.execute( "INSERT OR REPLACE INTO cached_items (key, value) VALUES (?1,?2)", (key, value), @@ -57,6 +64,19 @@ impl Persister { res } + pub fn set_wallet_info(&self, info: &GetInfoResponse) -> Result<()> { + let serialized_info = serde_json::to_string(info)?; + self.update_cached_item(KEY_WALLET_INFO, serialized_info) + } + + pub fn get_wallet_info(&self) -> Result> { + let info_str = self.get_cached_item(KEY_WALLET_INFO)?; + Ok(match info_str { + Some(str) => serde_json::from_str(str.as_str())?, + None => None, + }) + } + pub fn set_swapper_proxy_url(&self, swapper_proxy_url: String) -> Result<()> { self.update_cached_item(KEY_SWAPPER_PROXY_URL, swapper_proxy_url) } @@ -91,8 +111,24 @@ impl Persister { self.get_cached_item(KEY_WEBHOOK_URL) } + pub fn set_last_derivation_index_inner(&self, tx: &Transaction, index: u32) -> Result<()> { + Self::update_cached_item_inner(tx, KEY_LAST_DERIVATION_INDEX, index.to_string())?; + self.commit_outgoing( + tx, + LAST_DERIVATION_INDEX_DATA_ID, + RecordType::LastDerivationIndex, + // insert a mock updated field so that merging with incoming data works as expected + Some(vec![LAST_DERIVATION_INDEX_DATA_ID.to_string()]), + ) + } + pub fn set_last_derivation_index(&self, index: u32) -> Result<()> { - self.update_cached_item(KEY_LAST_DERIVATION_INDEX, index.to_string()) + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + self.set_last_derivation_index_inner(&tx, index)?; + tx.commit()?; + self.sync_trigger.try_send(())?; + Ok(()) } pub fn get_last_derivation_index(&self) -> Result> { @@ -109,16 +145,13 @@ impl Persister { .as_str() .parse::() .map(|index| index + 1)?; - Self::update_cached_item_inner( - &tx, - KEY_LAST_DERIVATION_INDEX, - next_index.to_string(), - )?; + self.set_last_derivation_index_inner(&tx, next_index)?; Some(next_index) } None => None, }; tx.commit()?; + self.sync_trigger.try_send(())?; Ok(res) } @@ -128,11 +161,11 @@ impl Persister { mod tests { use anyhow::Result; - use crate::test_utils::persist::new_persister; + use crate::test_utils::persist::create_persister; #[test] fn test_cached_items() -> Result<()> { - let (_temp_dir, persister) = new_persister()?; + create_persister!(persister); persister.update_cached_item("key1", "val1".to_string())?; let item_value = persister.get_cached_item("key1")?; @@ -147,7 +180,7 @@ mod tests { #[test] fn test_get_last_derivation_index() -> Result<()> { - let (_temp_dir, persister) = new_persister()?; + create_persister!(persister); let maybe_last_index = persister.get_last_derivation_index()?; assert!(maybe_last_index.is_none()); @@ -169,7 +202,7 @@ mod tests { #[test] fn test_next_derivation_index() -> Result<()> { - let (_temp_dir, persister) = new_persister()?; + create_persister!(persister); let maybe_next_index = persister.next_derivation_index()?; assert!(maybe_next_index.is_none()); diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 4ec2a5fe3..cec6ca00a 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -1,8 +1,6 @@ -use std::collections::HashMap; - use anyhow::Result; use boltz_client::swaps::boltz::{ChainSwapDetails, CreateChainResponse}; -use rusqlite::{named_params, params, Connection, Row}; +use rusqlite::{named_params, params, Connection, Row, TransactionBehavior}; use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash}; use serde::{Deserialize, Serialize}; @@ -10,20 +8,22 @@ use crate::ensure_sdk; use crate::error::PaymentError; use crate::model::*; use crate::persist::{get_where_clause_state_in, Persister}; +use crate::sync::model::RecordType; impl Persister { - pub(crate) fn insert_chain_swap(&self, chain_swap: &ChainSwap) -> Result<()> { - let con = self.get_connection()?; - + pub(crate) fn insert_or_update_chain_swap_inner( + con: &Connection, + chain_swap: &ChainSwap, + ) -> Result<()> { // There is a limit of 16 param elements in a single tuple in rusqlite, // so we split up the insert into two statements. - let mut stmt = con.prepare( + let id_hash = sha256::Hash::hash(chain_swap.id.as_bytes()).to_hex(); + con.execute( " INSERT INTO chain_swaps ( id, id_hash, direction, - claim_address, lockup_address, timeout_block_height, preimage, @@ -35,55 +35,76 @@ impl Persister { refund_private_key, claim_fees_sat, created_at, - state, - pair_fees_json + state ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT DO NOTHING", + ( + &chain_swap.id, + &id_hash, + &chain_swap.direction, + &chain_swap.lockup_address, + &chain_swap.timeout_block_height, + &chain_swap.preimage, + &chain_swap.payer_amount_sat, + &chain_swap.receiver_amount_sat, + &chain_swap.accept_zero_conf, + &chain_swap.create_response_json, + &chain_swap.claim_private_key, + &chain_swap.refund_private_key, + &chain_swap.claim_fees_sat, + &chain_swap.created_at, + &chain_swap.state, + ), )?; - let id_hash = sha256::Hash::hash(chain_swap.id.as_bytes()).to_hex(); - _ = stmt.execute(params![ - &chain_swap.id, - &id_hash, - &chain_swap.direction, - &chain_swap.claim_address, - &chain_swap.lockup_address, - &chain_swap.timeout_block_height, - &chain_swap.preimage, - &chain_swap.payer_amount_sat, - &chain_swap.receiver_amount_sat, - &chain_swap.accept_zero_conf, - &chain_swap.create_response_json, - &chain_swap.claim_private_key, - &chain_swap.refund_private_key, - &chain_swap.claim_fees_sat, - &chain_swap.created_at, - &chain_swap.state, - &chain_swap.pair_fees_json - ])?; con.execute( "UPDATE chain_swaps SET description = :description, + payer_amount_sat = :payer_amount_sat, + receiver_amount_sat = :receiver_amount_sat, + accept_zero_conf = :accept_zero_conf, server_lockup_tx_id = :server_lockup_tx_id, user_lockup_tx_id = :user_lockup_tx_id, + claim_address = :claim_address, claim_tx_id = :claim_tx_id, - refund_tx_id = :refund_tx_id + refund_tx_id = :refund_tx_id, + pair_fees_json = :pair_fees_json, + state = :state WHERE id = :id", named_params! { ":id": &chain_swap.id, ":description": &chain_swap.description, + ":payer_amount_sat": &chain_swap.payer_amount_sat, + ":receiver_amount_sat": &chain_swap.receiver_amount_sat, + ":accept_zero_conf": &chain_swap.accept_zero_conf, ":server_lockup_tx_id": &chain_swap.server_lockup_tx_id, ":user_lockup_tx_id": &chain_swap.user_lockup_tx_id, + ":claim_address": &chain_swap.claim_address, ":claim_tx_id": &chain_swap.claim_tx_id, ":refund_tx_id": &chain_swap.refund_tx_id, + ":pair_fees_json": &chain_swap.pair_fees_json, + ":state": &chain_swap.state, }, )?; Ok(()) } + pub(crate) fn insert_or_update_chain_swap(&self, chain_swap: &ChainSwap) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + Self::insert_or_update_chain_swap_inner(&tx, chain_swap)?; + self.commit_outgoing(&tx, &chain_swap.id, RecordType::Chain, None)?; + tx.commit()?; + self.sync_trigger.try_send(())?; + + Ok(()) + } + fn list_chain_swaps_query(where_clauses: Vec) -> String { let mut where_clause_str = String::new(); if !where_clauses.is_empty() { @@ -188,63 +209,29 @@ impl Persister { pub(crate) fn list_chain_swaps_by_state( &self, - con: &Connection, states: Vec, ) -> Result> { + let con = self.get_connection()?; let where_clause = vec![get_where_clause_state_in(&states)]; - self.list_chain_swaps_where(con, where_clause) + self.list_chain_swaps_where(&con, where_clause) } - pub(crate) fn list_ongoing_chain_swaps(&self, con: &Connection) -> Result> { - self.list_chain_swaps_by_state(con, vec![PaymentState::Created, PaymentState::Pending]) + pub(crate) fn list_ongoing_chain_swaps(&self) -> Result> { + let con = self.get_connection()?; + let where_clause = vec![get_where_clause_state_in(&[ + PaymentState::Created, + PaymentState::Pending, + ])]; + + self.list_chain_swaps_where(&con, where_clause) } pub(crate) fn list_pending_chain_swaps(&self) -> Result> { - let con: Connection = self.get_connection()?; - self.list_chain_swaps_by_state( - &con, - vec![PaymentState::Pending, PaymentState::RefundPending], - ) + self.list_chain_swaps_by_state(vec![PaymentState::Pending, PaymentState::RefundPending]) } pub(crate) fn list_refundable_chain_swaps(&self) -> Result> { - let con: Connection = self.get_connection()?; - self.list_chain_swaps_by_state(&con, vec![PaymentState::Refundable]) - } - - /// Pending Chain swaps, indexed by refund tx id - pub(crate) fn list_pending_chain_swaps_by_refund_tx_id( - &self, - ) -> Result> { - let res: HashMap = self - .list_pending_chain_swaps()? - .iter() - .filter_map(|pending_chain_swap| { - pending_chain_swap - .refund_tx_id - .as_ref() - .map(|refund_tx_id| (refund_tx_id.clone(), pending_chain_swap.clone())) - }) - .collect(); - Ok(res) - } - - /// This only returns the swaps that have a claim tx, skipping the pending ones that are being refunded. - pub(crate) fn list_pending_chain_swaps_by_claim_tx_id( - &self, - ) -> Result> { - let con: Connection = self.get_connection()?; - let res: HashMap = self - .list_chain_swaps_by_state(&con, vec![PaymentState::Pending])? - .iter() - .filter_map(|pending_chain_swap| { - pending_chain_swap - .claim_tx_id - .as_ref() - .map(|claim_tx_id| (claim_tx_id.clone(), pending_chain_swap.clone())) - }) - .collect(); - Ok(res) + self.list_chain_swaps_by_state(vec![PaymentState::Refundable]) } pub(crate) fn update_chain_swap_accept_zero_conf( @@ -252,8 +239,10 @@ impl Persister { swap_id: &str, accept_zero_conf: bool, ) -> Result<(), PaymentError> { - let con: Connection = self.get_connection()?; - con.execute( + let mut con: Connection = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + tx.execute( "UPDATE chain_swaps SET accept_zero_conf = :accept_zero_conf @@ -264,6 +253,19 @@ impl Persister { ":accept_zero_conf": accept_zero_conf, }, )?; + self.commit_outgoing( + &tx, + swap_id, + RecordType::Chain, + Some(vec!["accept_zero_conf".to_string()]), + )?; + tx.commit()?; + self.sync_trigger + .try_send(()) + .map_err(|err| PaymentError::Generic { + err: format!("Could not trigger manual sync: {err:?}"), + })?; + Ok(()) } @@ -276,8 +278,10 @@ impl Persister { receiver_amount_sat: u64, ) -> Result<(), PaymentError> { log::info!("Updating chain swap {swap_id}: payer_amount_sat = {payer_amount_sat}, receiver_amount_sat = {receiver_amount_sat}"); - let con: Connection = self.get_connection()?; - con.execute( + let mut con: Connection = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + tx.execute( "UPDATE chain_swaps SET payer_amount_sat = :payer_amount_sat, @@ -290,6 +294,22 @@ impl Persister { ":receiver_amount_sat": receiver_amount_sat, }, )?; + self.commit_outgoing( + &tx, + swap_id, + RecordType::Chain, + Some(vec![ + "payer_amount_sat".to_string(), + "receiver_amount_sat".to_string(), + ]), + )?; + tx.commit()?; + self.sync_trigger + .try_send(()) + .map_err(|err| PaymentError::Generic { + err: format!("Could not trigger manual sync: {err:?}"), + })?; + Ok(()) } @@ -341,16 +361,13 @@ impl Persister { pub(crate) fn try_handle_chain_swap_update( &self, - swap_id: &str, - to_state: PaymentState, - server_lockup_tx_id: Option<&str>, - user_lockup_tx_id: Option<&str>, - claim_tx_id: Option<&str>, - refund_tx_id: Option<&str>, + swap_update: &ChainSwapUpdate, ) -> Result<(), PaymentError> { - // Do not overwrite server_lockup_tx_id, user_lockup_tx_id, claim_tx_id, refund_tx_id - let con: Connection = self.get_connection()?; - con.execute( + // Do not overwrite server_lockup_tx_id, user_lockup_tx_id, claim_address, claim_tx_id, refund_tx_id + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + tx.execute( "UPDATE chain_swaps SET server_lockup_tx_id = @@ -365,6 +382,12 @@ impl Persister { ELSE user_lockup_tx_id END, + claim_address = + CASE + WHEN claim_address IS NULL THEN :claim_address + ELSE claim_address + END, + claim_tx_id = CASE WHEN claim_tx_id IS NULL THEN :claim_tx_id @@ -381,15 +404,18 @@ impl Persister { WHERE id = :id", named_params! { - ":id": swap_id, - ":server_lockup_tx_id": server_lockup_tx_id, - ":user_lockup_tx_id": user_lockup_tx_id, - ":claim_tx_id": claim_tx_id, - ":refund_tx_id": refund_tx_id, - ":state": to_state, + ":id": swap_update.swap_id, + ":server_lockup_tx_id": swap_update.server_lockup_tx_id, + ":user_lockup_tx_id": swap_update.user_lockup_tx_id, + ":claim_address": swap_update.claim_address, + ":claim_tx_id": swap_update.claim_tx_id, + ":refund_tx_id": swap_update.refund_tx_id, + ":state": swap_update.to_state, }, )?; + tx.commit()?; + Ok(()) } } diff --git a/lib/core/src/persist/migrations.rs b/lib/core/src/persist/migrations.rs index 8be3f1740..0b9bb500d 100644 --- a/lib/core/src/persist/migrations.rs +++ b/lib/core/src/persist/migrations.rs @@ -189,5 +189,30 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { ALTER TABLE send_swaps ADD COLUMN pair_fees_json TEXT NOT NULL DEFAULT ''; ALTER TABLE chain_swaps ADD COLUMN pair_fees_json TEXT NOT NULL DEFAULT ''; ", + "CREATE TABLE IF NOT EXISTS sync_state( + data_id TEXT NOT NULL PRIMARY KEY, + record_id TEXT NOT NULL, + record_revision INTEGER NOT NULL, + is_local INTEGER NOT NULL DEFAULT 1 + ) STRICT;", + "CREATE TABLE IF NOT EXISTS sync_settings( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL + ) STRICT;", + "CREATE TABLE IF NOT EXISTS sync_outgoing( + record_id TEXT NOT NULL PRIMARY KEY, + data_id TEXT NOT NULL UNIQUE, + record_type INTEGER NOT NULL, + commit_time INTEGER NOT NULL, + updated_fields_json TEXT + ) STRICT;", + "CREATE TABLE IF NOT EXISTS sync_incoming( + record_id TEXT NOT NULL PRIMARY KEY, + revision INTEGER NOT NULL UNIQUE, + schema_version TEXT NOT NULL, + data BLOB NOT NULL + ) STRICT;", + "ALTER TABLE receive_swaps DROP COLUMN mrh_script_pubkey;", + "ALTER TABLE payment_details ADD COLUMN lnurl_info_json TEXT;", ] } diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index be6324b68..30e88df42 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -1,29 +1,39 @@ mod address; mod backup; -mod cache; +pub(crate) mod cache; pub(crate) mod chain; mod migrations; +pub(crate) mod model; pub(crate) mod receive; pub(crate) mod send; +pub(crate) mod sync; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use std::ops::Not; use std::{fs::create_dir_all, path::PathBuf, str::FromStr}; -use crate::error::PaymentError; use crate::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use crate::model::*; +use crate::sync::model::RecordType; use crate::{get_invoice_description, utils}; use anyhow::{anyhow, Result}; use boltz_client::boltz::{ChainPair, ReversePair, SubmarinePair}; +use lwk_wollet::WalletTx; use migrations::current_migrations; -use rusqlite::{params, params_from_iter, Connection, OptionalExtension, Row, ToSql}; +use model::PaymentTxDetails; +use rusqlite::{ + params, params_from_iter, Connection, OptionalExtension, Row, ToSql, TransactionBehavior, +}; use rusqlite_migration::{Migrations, M}; +use sdk_common::bitcoin::hashes::hex::ToHex; +use tokio::sync::mpsc::Sender; const DEFAULT_DB_FILENAME: &str = "storage.sql"; pub(crate) struct Persister { main_db_dir: PathBuf, network: LiquidNetwork, + sync_trigger: Sender<()>, } /// Builds a WHERE clause that checks if `state` is any of the given arguments @@ -39,7 +49,11 @@ fn get_where_clause_state_in(allowed_states: &[PaymentState]) -> String { } impl Persister { - pub fn new(working_dir: &str, network: LiquidNetwork) -> Result { + pub fn new( + working_dir: &str, + network: LiquidNetwork, + sync_trigger: Sender<()>, + ) -> Result { let main_db_dir = PathBuf::from_str(working_dir)?; if !main_db_dir.exists() { create_dir_all(&main_db_dir)?; @@ -47,6 +61,7 @@ impl Persister { Ok(Persister { main_db_dir, network, + sync_trigger, }) } @@ -86,15 +101,74 @@ impl Persister { } } + pub(crate) fn insert_or_update_payment_with_wallet_tx(&self, tx: &WalletTx) -> Result<()> { + let tx_id = tx.txid.to_string(); + let is_tx_confirmed = tx.height.is_some(); + let amount_sat = tx.balance.values().sum::(); + let maybe_script_pubkey = tx + .outputs + .iter() + .find(|output| output.is_some()) + .and_then(|output| output.clone().map(|o| o.script_pubkey.to_hex())); + self.insert_or_update_payment( + PaymentTxData { + tx_id: tx_id.clone(), + timestamp: tx.timestamp, + amount_sat: amount_sat.unsigned_abs(), + fees_sat: tx.fee, + payment_type: match amount_sat >= 0 { + true => PaymentType::Receive, + false => PaymentType::Send, + }, + is_confirmed: is_tx_confirmed, + }, + maybe_script_pubkey.map(|destination| PaymentTxDetails { + tx_id, + destination, + ..Default::default() + }), + true, + ) + } + + pub(crate) fn list_unconfirmed_payment_txs_data(&self) -> Result> { + let con = self.get_connection()?; + let mut stmt = con.prepare( + "SELECT tx_id, + timestamp, + amount_sat, + fees_sat, + payment_type, + is_confirmed + FROM payment_tx_data + WHERE is_confirmed = 0", + )?; + let payments: Vec = stmt + .query_map([], |row| { + Ok(PaymentTxData { + tx_id: row.get(0)?, + timestamp: row.get(1)?, + amount_sat: row.get(2)?, + fees_sat: row.get(3)?, + payment_type: row.get(4)?, + is_confirmed: row.get(5)?, + }) + })? + .map(|i| i.unwrap()) + .collect(); + Ok(payments) + } + pub(crate) fn insert_or_update_payment( &self, ptx: PaymentTxData, - destination: Option, - description: Option, - ) -> Result<(), PaymentError> { - let con = self.get_connection()?; - con.execute( - "INSERT OR REPLACE INTO payment_tx_data ( + payment_tx_details: Option, + from_wallet_tx_data: bool, + ) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + tx.execute( + "INSERT INTO payment_tx_data ( tx_id, timestamp, amount_sat, @@ -103,10 +177,16 @@ impl Persister { is_confirmed ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (tx_id) + DO UPDATE SET timestamp = CASE WHEN excluded.is_confirmed = 1 THEN excluded.timestamp ELSE timestamp END, + amount_sat = excluded.amount_sat, + fees_sat = excluded.fees_sat, + payment_type = excluded.payment_type, + is_confirmed = excluded.is_confirmed ", ( &ptx.tx_id, - ptx.timestamp, + ptx.timestamp.or(Some(utils::now())), ptx.amount_sat, ptx.fees_sat, ptx.payment_type, @@ -114,40 +194,130 @@ impl Persister { ), )?; - if let Some(destination) = destination { - // Only store the destination if there is no payment_details entry else - // the destination is overwritten by the tx script_pubkey - con.execute( - "INSERT INTO payment_details ( - tx_id, - destination, - description - ) - VALUES (?1, ?2, ?3) - ON CONFLICT (tx_id) - DO UPDATE SET description = COALESCE(?3, description) - ", - (ptx.tx_id, destination, description), + let mut trigger_sync = false; + if let Some(ref payment_tx_details) = payment_tx_details { + // If the update comes from the wallet tx: + // - Skip updating the destination from the script_pubkey + // - Skip syncing the payment_tx_details + Self::insert_or_update_payment_details_inner( + &tx, + payment_tx_details, + from_wallet_tx_data, )?; + if !from_wallet_tx_data { + self.commit_outgoing( + &tx, + &payment_tx_details.tx_id, + RecordType::PaymentDetails, + None, + )?; + trigger_sync = true; + } + } + + tx.commit()?; + + if trigger_sync { + self.sync_trigger.try_send(())?; } Ok(()) } - pub(crate) fn list_ongoing_swaps(&self) -> Result> { + fn insert_or_update_payment_details_inner( + con: &Connection, + payment_tx_details: &PaymentTxDetails, + skip_destination_update: bool, + ) -> Result<()> { + let destination_update = skip_destination_update + .not() + .then_some("destination = excluded.destination,") + .unwrap_or_default(); + con.execute( + &format!( + "INSERT INTO payment_details ( + tx_id, + destination, + description, + lnurl_info_json + ) + VALUES (?, ?, ?, ?) + ON CONFLICT (tx_id) + DO UPDATE SET + {destination_update} + description = COALESCE(excluded.description, description), + lnurl_info_json = COALESCE(excluded.lnurl_info_json, lnurl_info_json) + " + ), + ( + &payment_tx_details.tx_id, + &payment_tx_details.destination, + &payment_tx_details.description, + payment_tx_details + .lnurl_info + .as_ref() + .map(|info| serde_json::to_string(&info).ok()), + ), + )?; + Ok(()) + } + + pub(crate) fn insert_or_update_payment_details( + &self, + payment_tx_details: PaymentTxDetails, + ) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + Self::insert_or_update_payment_details_inner(&tx, &payment_tx_details, false)?; + self.commit_outgoing( + &tx, + &payment_tx_details.tx_id, + RecordType::PaymentDetails, + None, + )?; + tx.commit()?; + + self.sync_trigger.try_send(())?; + + Ok(()) + } + + pub(crate) fn get_payment_details(&self, tx_id: &str) -> Result> { let con = self.get_connection()?; + let mut stmt = con.prepare( + "SELECT destination, description, lnurl_info_json + FROM payment_details + WHERE tx_id = ?", + )?; + let res = stmt.query_row([tx_id], |row| { + let destination = row.get(0)?; + let description = row.get(1)?; + let maybe_lnurl_info_json: Option = row.get(2)?; + Ok(PaymentTxDetails { + tx_id: tx_id.to_string(), + destination, + description, + lnurl_info: maybe_lnurl_info_json + .and_then(|info| serde_json::from_str::(&info).ok()), + }) + }); + Ok(res.ok()) + } + + pub(crate) fn list_ongoing_swaps(&self) -> Result> { let ongoing_send_swaps: Vec = self - .list_ongoing_send_swaps(&con)? + .list_ongoing_send_swaps()? .into_iter() .map(Swap::Send) .collect(); let ongoing_receive_swaps: Vec = self - .list_ongoing_receive_swaps(&con)? + .list_ongoing_receive_swaps()? .into_iter() .map(Swap::Receive) .collect(); let ongoing_chain_swaps: Vec = self - .list_ongoing_chain_swaps(&con)? + .list_ongoing_chain_swaps()? .into_iter() .map(Swap::Chain) .collect(); @@ -209,25 +379,33 @@ impl Persister { cs.pair_fees_json, rtx.amount_sat, pd.destination, - pd.description + pd.description, + pd.lnurl_info_json FROM payment_tx_data AS ptx -- Payment tx (each tx results in a Payment) FULL JOIN ( SELECT * FROM receive_swaps - WHERE COALESCE(claim_tx_id, lockup_tx_id, mrh_tx_id) IS NOT NULL - ) rs -- Receive Swap data (by claim) + WHERE + COALESCE(claim_tx_id, lockup_tx_id, mrh_tx_id) IS NOT NULL + AND state NOT IN (0, 3, 4) -- Ignore Created, Failed and TimedOut + ) rs -- Receive Swap data ON ptx.tx_id in (rs.claim_tx_id, rs.mrh_tx_id) + FULL JOIN ( + SELECT * FROM chain_swaps + WHERE + COALESCE(user_lockup_tx_id, claim_tx_id) IS NOT NULL + AND state NOT IN (0, 4) -- Ignore Created and TimedOut + ) cs -- Chain Swap data + ON ptx.tx_id in (cs.user_lockup_tx_id, cs.claim_tx_id) LEFT JOIN send_swaps AS ss -- Send Swap data ON ptx.tx_id = ss.lockup_tx_id - LEFT JOIN chain_swaps AS cs -- Chain Swap data - ON ptx.tx_id in (cs.user_lockup_tx_id, cs.claim_tx_id) LEFT JOIN payment_tx_data AS rtx -- Refund tx data ON rtx.tx_id in (ss.refund_tx_id, cs.refund_tx_id) LEFT JOIN payment_details AS pd -- Payment details ON pd.tx_id = ptx.tx_id - WHERE -- Filter out refund txs from Send Swaps - ptx.tx_id NOT IN (SELECT refund_tx_id FROM send_swaps WHERE refund_tx_id NOT NULL) - AND -- Filter out refund txs from Chain Swaps - ptx.tx_id NOT IN (SELECT refund_tx_id FROM chain_swaps WHERE refund_tx_id NOT NULL) + WHERE + (ptx.tx_id IS NULL -- Filter out refund txs from Chain/Send Swaps + OR ptx.tx_id NOT IN (SELECT refund_tx_id FROM send_swaps WHERE refund_tx_id NOT NULL) + AND ptx.tx_id NOT IN (SELECT refund_tx_id FROM chain_swaps WHERE refund_tx_id NOT NULL)) AND {} ORDER BY -- Order by swap creation time or tx timestamp (in case of direct tx) COALESCE(rs.created_at, ss.created_at, cs.created_at, ptx.timestamp) DESC @@ -300,6 +478,9 @@ impl Persister { let maybe_payment_details_destination: Option = row.get(40)?; let maybe_payment_details_description: Option = row.get(41)?; + let maybe_payment_details_lnurl_info_json: Option = row.get(42)?; + let maybe_payment_details_lnurl_info: Option = + maybe_payment_details_lnurl_info_json.and_then(|info| serde_json::from_str(&info).ok()); let (swap, payment_type) = match maybe_receive_swap_id { Some(receive_swap_id) => { @@ -431,6 +612,7 @@ impl Persister { bolt11, bolt12_offer, payment_hash, + lnurl_info: maybe_payment_details_lnurl_info, refund_tx_id, refund_tx_amount_sat, description: description.unwrap_or("Lightning transfer".to_string()), @@ -457,7 +639,11 @@ impl Persister { match (tx, swap.clone()) { (None, None) => Err(maybe_tx_tx_id.err().unwrap()), - (None, Some(swap)) => Ok(Payment::from_pending_swap(swap, payment_type)), + (None, Some(swap)) => Ok(Payment::from_pending_swap( + swap, + payment_type, + payment_details, + )), (Some(tx), None) => Ok(Payment::from_tx_data(tx, None, payment_details)), (Some(tx), Some(swap)) => Ok(Payment::from_tx_data(tx, Some(swap), payment_details)), } @@ -467,7 +653,11 @@ impl Persister { Ok(self .get_connection()? .query_row( - &self.select_payment_query(Some("ptx.tx_id = ?1"), None, None), + &self.select_payment_query( + Some("(ptx.tx_id = ?1 OR COALESCE(rs.id, ss.id, cs.id) = ?1)"), + None, + None, + ), params![id], |row| self.sql_row_to_payment(row), ) @@ -510,6 +700,28 @@ impl Persister { .collect(); Ok(payments) } + + pub fn get_payments_by_tx_id( + &self, + req: &ListPaymentsRequest, + ) -> Result> { + let res: HashMap = self + .get_payments(req)? + .into_iter() + .flat_map(|payment| { + // Index payments by both tx_id (lockup/claim) and refund_tx_id + let mut res = vec![]; + if let Some(tx_id) = payment.tx_id.clone() { + res.push((tx_id, payment.clone())); + } + if let Some(refund_tx_id) = payment.get_refund_tx_id() { + res.push((refund_tx_id, payment)); + } + res + }) + .collect(); + Ok(res) + } } fn filter_to_where_clause(req: &ListPaymentsRequest) -> (String, Vec>) { @@ -544,6 +756,20 @@ fn filter_to_where_clause(req: &ListPaymentsRequest) -> (String, Vec = HashSet::from_iter(states.iter().map(|s| *s as i8)); + where_clause.push(format!( + "COALESCE(rs.state, ss.state, cs.state) in ({})", + states_hash + .iter() + .map(|t| format!("{}", t)) + .collect::>() + .join(", ") + )); + } + } + if let Some(details) = &req.details { match details { ListPaymentDetails::Bitcoin { address } => { @@ -570,9 +796,10 @@ mod tests { use anyhow::Result; use crate::{ + persist::PaymentTxDetails, prelude::ListPaymentsRequest, test_utils::persist::{ - new_payment_tx_data, new_persister, new_receive_swap, new_send_swap, + create_persister, new_payment_tx_data, new_receive_swap, new_send_swap, }, }; @@ -580,13 +807,16 @@ mod tests { #[test] fn test_get_payments() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let payment_tx_data = new_payment_tx_data(PaymentType::Send); storage.insert_or_update_payment( payment_tx_data.clone(), - Some("mock-address".to_string()), - None, + Some(PaymentTxDetails { + destination: "mock-address".to_string(), + ..Default::default() + }), + false, )?; assert!(storage @@ -602,10 +832,10 @@ mod tests { #[test] fn test_list_ongoing_swaps() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); - storage.insert_send_swap(&new_send_swap(None))?; - storage.insert_receive_swap(&new_receive_swap(Some(PaymentState::Pending)))?; + storage.insert_or_update_send_swap(&new_send_swap(None))?; + storage.insert_or_update_receive_swap(&new_receive_swap(Some(PaymentState::Pending)))?; assert_eq!(storage.list_ongoing_swaps()?.len(), 2); diff --git a/lib/core/src/persist/model.rs b/lib/core/src/persist/model.rs new file mode 100644 index 000000000..309d72578 --- /dev/null +++ b/lib/core/src/persist/model.rs @@ -0,0 +1,9 @@ +use super::LnUrlInfo; + +#[derive(Clone, Debug, Default)] +pub(crate) struct PaymentTxDetails { + pub(crate) tx_id: String, + pub(crate) destination: String, + pub(crate) description: Option, + pub(crate) lnurl_info: Option, +} diff --git a/lib/core/src/persist/receive.rs b/lib/core/src/persist/receive.rs index 57aa4ed7f..2f6d81367 100644 --- a/lib/core/src/persist/receive.rs +++ b/lib/core/src/persist/receive.rs @@ -1,8 +1,6 @@ -use std::collections::HashMap; - use anyhow::Result; use boltz_client::swaps::boltz::CreateReverseResponse; -use rusqlite::{named_params, params, Connection, Row}; +use rusqlite::{named_params, params, Connection, Row, TransactionBehavior}; use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash}; use serde::{Deserialize, Serialize}; @@ -10,12 +8,15 @@ use crate::ensure_sdk; use crate::error::PaymentError; use crate::model::*; use crate::persist::{get_where_clause_state_in, Persister}; +use crate::sync::model::RecordType; impl Persister { - pub(crate) fn insert_receive_swap(&self, receive_swap: &ReceiveSwap) -> Result<()> { - let con = self.get_connection()?; - - let mut stmt = con.prepare( + pub(crate) fn insert_or_update_receive_swap_inner( + con: &Connection, + receive_swap: &ReceiveSwap, + ) -> Result<()> { + let id_hash = sha256::Hash::hash(receive_swap.id.as_bytes()).to_hex(); + con.execute( " INSERT INTO receive_swaps ( id, @@ -30,47 +31,66 @@ impl Persister { created_at, claim_fees_sat, mrh_address, - mrh_script_pubkey, state, pair_fees_json ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT DO NOTHING + ", + ( + &receive_swap.id, + id_hash, + &receive_swap.preimage, + &receive_swap.create_response_json, + &receive_swap.claim_private_key, + &receive_swap.invoice, + &receive_swap.payment_hash, + &receive_swap.payer_amount_sat, + &receive_swap.receiver_amount_sat, + &receive_swap.created_at, + &receive_swap.claim_fees_sat, + &receive_swap.mrh_address, + &receive_swap.state, + &receive_swap.pair_fees_json, + ), )?; - let id_hash = sha256::Hash::hash(receive_swap.id.as_bytes()).to_hex(); - _ = stmt.execute(( - &receive_swap.id, - id_hash, - &receive_swap.preimage, - &receive_swap.create_response_json, - &receive_swap.claim_private_key, - &receive_swap.invoice, - &receive_swap.payment_hash, - &receive_swap.payer_amount_sat, - &receive_swap.receiver_amount_sat, - &receive_swap.created_at, - &receive_swap.claim_fees_sat, - &receive_swap.mrh_address, - &receive_swap.mrh_script_pubkey, - &receive_swap.state, - &receive_swap.pair_fees_json, - ))?; con.execute( "UPDATE receive_swaps SET description = :description, claim_tx_id = :claim_tx_id, - mrh_tx_id = :mrh_tx_id + lockup_tx_id = :lockup_tx_id, + mrh_tx_id = :mrh_tx_id, + state = :state WHERE id = :id", named_params! { ":id": &receive_swap.id, ":description": &receive_swap.description, ":claim_tx_id": &receive_swap.claim_tx_id, + ":lockup_tx_id": &receive_swap.lockup_tx_id, ":mrh_tx_id": &receive_swap.mrh_tx_id, + ":state": &receive_swap.state, }, )?; + if receive_swap.mrh_tx_id.is_some() { + Self::delete_reserved_address_inner(con, &receive_swap.mrh_address)?; + } + + Ok(()) + } + + pub(crate) fn insert_or_update_receive_swap(&self, receive_swap: &ReceiveSwap) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + Self::insert_or_update_receive_swap_inner(&tx, receive_swap)?; + self.commit_outgoing(&tx, &receive_swap.id, RecordType::Receive, None)?; + tx.commit()?; + self.sync_trigger.try_send(())?; + Ok(()) } @@ -97,7 +117,6 @@ impl Persister { rs.claim_tx_id, rs.lockup_tx_id, rs.mrh_address, - rs.mrh_script_pubkey, rs.mrh_tx_id, rs.created_at, rs.state, @@ -143,19 +162,13 @@ impl Persister { claim_tx_id: row.get(10)?, lockup_tx_id: row.get(11)?, mrh_address: row.get(12)?, - mrh_script_pubkey: row.get(13)?, - mrh_tx_id: row.get(14)?, - created_at: row.get(15)?, - state: row.get(16)?, - pair_fees_json: row.get(17)?, + mrh_tx_id: row.get(13)?, + created_at: row.get(14)?, + state: row.get(15)?, + pair_fees_json: row.get(16)?, }) } - pub(crate) fn list_receive_swaps(&self) -> Result> { - let con: Connection = self.get_connection()?; - self.list_receive_swaps_where(&con, vec![]) - } - pub(crate) fn list_receive_swaps_where( &self, con: &Connection, @@ -170,66 +183,24 @@ impl Persister { Ok(ongoing_receive) } - pub(crate) fn list_ongoing_receive_swaps(&self, con: &Connection) -> Result> { + pub(crate) fn list_ongoing_receive_swaps(&self) -> Result> { + let con = self.get_connection()?; let where_clause = vec![get_where_clause_state_in(&[ PaymentState::Created, PaymentState::Pending, ])]; - self.list_receive_swaps_where(con, where_clause) + self.list_receive_swaps_where(&con, where_clause) } - pub(crate) fn list_pending_receive_swaps(&self) -> Result> { - let con: Connection = self.get_connection()?; - let query = Self::list_receive_swaps_query(vec!["state = ?1".to_string()]); - let res = con - .prepare(&query)? - .query_map( - params![PaymentState::Pending], - Self::sql_row_to_receive_swap, - )? - .map(|i| i.unwrap()) - .collect(); - Ok(res) - } - - /// Ongoing Receive Swaps with no claim or lockup transactions, indexed by mrh_script_pubkey - pub(crate) fn list_ongoing_receive_swaps_by_mrh_script_pubkey( - &self, - ) -> Result> { - let con: Connection = self.get_connection()?; - let res = self - .list_ongoing_receive_swaps(&con)? - .iter() - .filter_map(|swap| { - match ( - swap.lockup_tx_id.clone(), - swap.claim_tx_id.clone(), - swap.mrh_script_pubkey.is_empty(), - ) { - (None, None, false) => Some((swap.mrh_script_pubkey.clone(), swap.clone())), - _ => None, - } - }) - .collect(); - Ok(res) - } + pub(crate) fn list_recoverable_receive_swaps(&self) -> Result> { + let con = self.get_connection()?; + let where_clause = vec![get_where_clause_state_in(&[ + PaymentState::Created, + PaymentState::Pending, + ])]; - /// Pending Receive Swaps, indexed by claim_tx_id - pub(crate) fn list_pending_receive_swaps_by_claim_tx_id( - &self, - ) -> Result> { - let res = self - .list_pending_receive_swaps()? - .iter() - .filter_map(|pending_receive_swap| { - pending_receive_swap - .claim_tx_id - .as_ref() - .map(|claim_tx_id| (claim_tx_id.clone(), pending_receive_swap.clone())) - }) - .collect(); - Ok(res) + self.list_receive_swaps_where(&con, where_clause) } // Only set the Receive Swap claim_tx_id if not set, otherwise return an error @@ -286,8 +257,10 @@ impl Persister { mrh_amount_sat: Option, ) -> Result<(), PaymentError> { // Do not overwrite claim_tx_id or lockup_tx_id - let con: Connection = self.get_connection()?; - con.execute( + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; + + tx.execute( "UPDATE receive_swaps SET claim_tx_id = @@ -320,6 +293,17 @@ impl Persister { }, )?; + // NOTE: Receive currently does not update any fields, bypassing the commit logic for now + // let updated_fields = None; + // Self::commit_outgoing(&tx, swap_id, RecordType::Receive, updated_fields)?; + // self.sync_trigger + // .try_send(()) + // .map_err(|err| PaymentError::Generic { + // err: format!("Could not trigger manual sync: {err:?}"), + // })?; + + tx.commit()?; + Ok(()) } } @@ -366,17 +350,17 @@ impl InternalCreateReverseResponse { mod tests { use anyhow::{anyhow, Result}; - use crate::test_utils::persist::{new_persister, new_receive_swap}; + use crate::test_utils::persist::{create_persister, new_receive_swap}; use super::PaymentState; #[test] fn test_fetch_receive_swap() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let receive_swap = new_receive_swap(None); - storage.insert_receive_swap(&receive_swap)?; + storage.insert_or_update_receive_swap(&receive_swap)?; // Fetch swap by id assert!(storage.fetch_receive_swap_by_id(&receive_swap.id).is_ok()); // Fetch swap by invoice @@ -389,12 +373,12 @@ mod tests { #[test] fn test_list_receive_swap() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); // List general receive swaps let range = 0..3; for _ in range.clone() { - storage.insert_receive_swap(&new_receive_swap(None))?; + storage.insert_or_update_receive_swap(&new_receive_swap(None))?; } let con = storage.get_connection()?; @@ -402,23 +386,19 @@ mod tests { assert_eq!(swaps.len(), range.len()); // List ongoing receive swaps - storage.insert_receive_swap(&new_receive_swap(Some(PaymentState::Pending)))?; - let ongoing_swaps = storage.list_ongoing_receive_swaps(&con)?; + storage.insert_or_update_receive_swap(&new_receive_swap(Some(PaymentState::Pending)))?; + let ongoing_swaps = storage.list_ongoing_receive_swaps()?; assert_eq!(ongoing_swaps.len(), 4); - // List pending receive swaps - let ongoing_swaps = storage.list_pending_receive_swaps()?; - assert_eq!(ongoing_swaps.len(), 1); - Ok(()) } #[test] fn test_update_receive_swap() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let receive_swap = new_receive_swap(None); - storage.insert_receive_swap(&receive_swap)?; + storage.insert_or_update_receive_swap(&receive_swap)?; // Update metadata let new_state = PaymentState::Pending; diff --git a/lib/core/src/persist/send.rs b/lib/core/src/persist/send.rs index 429dd53cd..0f36766c1 100644 --- a/lib/core/src/persist/send.rs +++ b/lib/core/src/persist/send.rs @@ -1,21 +1,22 @@ -use std::collections::HashMap; - use anyhow::Result; use boltz_client::swaps::boltz::CreateSubmarineResponse; use rusqlite::{named_params, params, Connection, Row}; use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash}; use serde::{Deserialize, Serialize}; -use crate::ensure_sdk; use crate::error::PaymentError; use crate::model::*; use crate::persist::{get_where_clause_state_in, Persister}; +use crate::sync::model::RecordType; +use crate::{ensure_sdk, get_updated_fields}; impl Persister { - pub(crate) fn insert_send_swap(&self, send_swap: &SendSwap) -> Result<()> { - let con = self.get_connection()?; - - let mut stmt = con.prepare( + pub(crate) fn insert_or_update_send_swap_inner( + con: &Connection, + send_swap: &SendSwap, + ) -> Result<()> { + let id_hash = sha256::Hash::hash(send_swap.id.as_bytes()).to_hex(); + con.execute( " INSERT INTO send_swaps ( id, @@ -23,37 +24,64 @@ impl Persister { invoice, bolt12_offer, payment_hash, - description, payer_amount_sat, receiver_amount_sat, create_response_json, refund_private_key, - lockup_tx_id, - refund_tx_id, created_at, state, pair_fees_json ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT DO NOTHING + ", + ( + &send_swap.id, + &id_hash, + &send_swap.invoice, + &send_swap.bolt12_offer, + &send_swap.payment_hash, + &send_swap.payer_amount_sat, + &send_swap.receiver_amount_sat, + &send_swap.create_response_json, + &send_swap.refund_private_key, + &send_swap.created_at, + &send_swap.state, + &send_swap.pair_fees_json, + ), + )?; + + con.execute( + "UPDATE send_swaps + SET + description = :description, + preimage = :preimage, + lockup_tx_id = :lockup_tx_id, + refund_tx_id = :refund_tx_id, + state = :state + WHERE + id = :id", + named_params! { + ":id": &send_swap.id, + ":description": &send_swap.description, + ":preimage": &send_swap.preimage, + ":lockup_tx_id": &send_swap.lockup_tx_id, + ":refund_tx_id": &send_swap.refund_tx_id, + ":state": &send_swap.state, + }, )?; - let id_hash = sha256::Hash::hash(send_swap.id.as_bytes()).to_hex(); - _ = stmt.execute(( - &send_swap.id, - &id_hash, - &send_swap.invoice, - &send_swap.bolt12_offer, - &send_swap.payment_hash, - &send_swap.description, - &send_swap.payer_amount_sat, - &send_swap.receiver_amount_sat, - &send_swap.create_response_json, - &send_swap.refund_private_key, - &send_swap.lockup_tx_id, - &send_swap.refund_tx_id, - &send_swap.created_at, - &send_swap.state, - &send_swap.pair_fees_json, - ))?; + + Ok(()) + } + + pub(crate) fn insert_or_update_send_swap(&self, send_swap: &SendSwap) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; + + Self::insert_or_update_send_swap_inner(&tx, send_swap)?; + self.commit_outgoing(&tx, &send_swap.id, RecordType::Send, None)?; + tx.commit()?; + self.sync_trigger.try_send(())?; Ok(()) } @@ -148,11 +176,6 @@ impl Persister { }) } - pub(crate) fn list_send_swaps(&self) -> Result> { - let con: Connection = self.get_connection()?; - self.list_send_swaps_where(&con, vec![]) - } - pub(crate) fn list_send_swaps_where( &self, con: &Connection, @@ -167,13 +190,14 @@ impl Persister { Ok(ongoing_send) } - pub(crate) fn list_ongoing_send_swaps(&self, con: &Connection) -> Result> { + pub(crate) fn list_ongoing_send_swaps(&self) -> Result> { + let con = self.get_connection()?; let where_clause = vec![get_where_clause_state_in(&[ PaymentState::Created, PaymentState::Pending, ])]; - self.list_send_swaps_where(con, where_clause) + self.list_send_swaps_where(&con, where_clause) } pub(crate) fn list_pending_send_swaps(&self) -> Result> { @@ -185,21 +209,13 @@ impl Persister { self.list_send_swaps_where(&con, where_clause) } - /// Pending Send swaps, indexed by refund tx id - pub(crate) fn list_pending_send_swaps_by_refund_tx_id( - &self, - ) -> Result> { - let res: HashMap = self - .list_pending_send_swaps()? - .iter() - .filter_map(|pending_send_swap| { - pending_send_swap - .refund_tx_id - .as_ref() - .map(|refund_tx_id| (refund_tx_id.clone(), pending_send_swap.clone())) - }) - .collect(); - Ok(res) + pub(crate) fn list_recoverable_send_swaps(&self) -> Result> { + let con = self.get_connection()?; + let where_clause = vec![get_where_clause_state_in(&[ + PaymentState::Pending, + PaymentState::RefundPending, + ])]; + self.list_send_swaps_where(&con, where_clause) } pub(crate) fn try_handle_send_swap_update( @@ -211,8 +227,10 @@ impl Persister { refund_tx_id: Option<&str>, ) -> Result<(), PaymentError> { // Do not overwrite preimage, lockup_tx_id, refund_tx_id - let con: Connection = self.get_connection()?; - con.execute( + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?; + + tx.execute( "UPDATE send_swaps SET preimage = @@ -245,6 +263,15 @@ impl Persister { }, )?; + let updated_fields = get_updated_fields!(preimage); + self.commit_outgoing(&tx, swap_id, RecordType::Send, updated_fields)?; + tx.commit()?; + self.sync_trigger + .try_send(()) + .map_err(|err| PaymentError::Generic { + err: format!("Could not trigger manual sync: {err:?}"), + })?; + Ok(()) } @@ -336,16 +363,16 @@ impl InternalCreateSubmarineResponse { mod tests { use anyhow::{anyhow, Result}; - use crate::test_utils::persist::{new_persister, new_send_swap}; + use crate::test_utils::persist::{create_persister, new_send_swap}; use super::PaymentState; #[test] fn test_fetch_send_swap() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let send_swap = new_send_swap(None); - storage.insert_send_swap(&send_swap)?; + storage.insert_or_update_send_swap(&send_swap)?; // Fetch swap by id assert!(storage.fetch_send_swap_by_id(&send_swap.id).is_ok()); // Fetch swap by invoice @@ -358,12 +385,12 @@ mod tests { #[test] fn test_list_send_swap() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); // List general send swaps let range = 0..3; for _ in range.clone() { - storage.insert_send_swap(&new_send_swap(None))?; + storage.insert_or_update_send_swap(&new_send_swap(None))?; } let con = storage.get_connection()?; @@ -371,8 +398,8 @@ mod tests { assert_eq!(swaps.len(), range.len()); // List ongoing send swaps - storage.insert_send_swap(&new_send_swap(Some(PaymentState::Pending)))?; - let ongoing_swaps = storage.list_ongoing_send_swaps(&con)?; + storage.insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending)))?; + let ongoing_swaps = storage.list_ongoing_send_swaps()?; assert_eq!(ongoing_swaps.len(), 4); // List pending send swaps @@ -384,10 +411,10 @@ mod tests { #[test] fn test_update_send_swap() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; + create_persister!(storage); let mut send_swap = new_send_swap(None); - storage.insert_send_swap(&send_swap)?; + storage.insert_or_update_send_swap(&send_swap)?; // Update metadata let new_state = PaymentState::Pending; diff --git a/lib/core/src/persist/sync.rs b/lib/core/src/persist/sync.rs new file mode 100644 index 000000000..f9d27b49c --- /dev/null +++ b/lib/core/src/persist/sync.rs @@ -0,0 +1,496 @@ +use std::collections::HashMap; + +use anyhow::Result; +use rusqlite::{ + named_params, Connection, OptionalExtension, Row, Statement, Transaction, TransactionBehavior, +}; + +use super::{cache::KEY_LAST_DERIVATION_INDEX, PaymentTxDetails, Persister, Swap}; +use crate::{ + sync::model::{ + data::LAST_DERIVATION_INDEX_DATA_ID, sync::Record, RecordType, SyncOutgoingChanges, + SyncSettings, SyncState, + }, + utils, +}; + +impl Persister { + fn select_sync_state_query(where_clauses: Vec) -> String { + let mut where_clause_str = String::new(); + if !where_clauses.is_empty() { + where_clause_str = String::from("WHERE "); + where_clause_str.push_str(where_clauses.join(" AND ").as_str()); + } + + format!( + " + SELECT + data_id, + record_id, + record_revision, + is_local + FROM sync_state + {where_clause_str} + " + ) + } + + fn sql_row_to_sync_state(row: &Row) -> rusqlite::Result { + Ok(SyncState { + data_id: row.get(0)?, + record_id: row.get(1)?, + record_revision: row.get(2)?, + is_local: row.get(3)?, + }) + } + + pub(crate) fn get_sync_state_by_record_id(&self, record_id: &str) -> Result> { + let con = self.get_connection()?; + let query = Self::select_sync_state_query(vec!["record_id = ?1".to_string()]); + let sync_state = con + .query_row(&query, [record_id], Self::sql_row_to_sync_state) + .optional()?; + Ok(sync_state) + } + + pub(crate) fn get_sync_state_by_data_id(&self, data_id: &str) -> Result> { + let con = self.get_connection()?; + let query = Self::select_sync_state_query(vec!["data_id = ?1".to_string()]); + let sync_state = con + .query_row(&query, [data_id], Self::sql_row_to_sync_state) + .optional()?; + Ok(sync_state) + } + + fn set_sync_state_stmt(con: &Connection) -> rusqlite::Result { + con.prepare( + " + INSERT OR REPLACE INTO sync_state(data_id, record_id, record_revision, is_local) + VALUES (:data_id, :record_id, :record_revision, :is_local) + ", + ) + } + + pub(crate) fn set_sync_state(&self, sync_state: SyncState) -> Result<()> { + let con = self.get_connection()?; + + Self::set_sync_state_stmt(&con)?.execute(named_params! { + ":data_id": &sync_state.data_id, + ":record_id": &sync_state.record_id, + ":record_revision": &sync_state.record_revision, + ":is_local": &sync_state.is_local, + })?; + + Ok(()) + } + + pub(crate) fn get_sync_settings(&self) -> Result { + let con = self.get_connection()?; + + let settings: HashMap = con + .prepare("SELECT key, value FROM sync_settings")? + .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + .map(|e| e.unwrap()) + .collect(); + + let latest_revision = match settings.get("latest_revision") { + Some(revision) => Some(revision.parse()?), + None => None, + }; + + let sync_settings = SyncSettings { + remote_url: settings.get("remote_url").cloned(), + latest_revision, + }; + + Ok(sync_settings) + } + + fn set_sync_setting_stmt(con: &Connection) -> rusqlite::Result { + con.prepare("INSERT OR REPLACE INTO sync_settings(key, value) VALUES(:key, :value)") + } + + pub(crate) fn set_sync_settings(&self, map: HashMap<&'static str, String>) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + for (key, value) in map { + Self::set_sync_setting_stmt(&tx)?.execute(named_params! { + ":key": key, + ":value": value, + })?; + } + + tx.commit()?; + + Ok(()) + } + + pub(crate) fn set_new_remote(&self, remote_url: String) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + tx.execute("DELETE FROM sync_state", [])?; + tx.execute("DELETE FROM sync_incoming", [])?; + tx.execute("DELETE FROM sync_outgoing", [])?; + + let swap_tables = HashMap::from([ + ("receive_swaps", RecordType::Receive), + ("send_swaps", RecordType::Send), + ("chain_swaps", RecordType::Chain), + ]); + for (table_name, record_type) in swap_tables { + let mut stmt = tx.prepare(&format!("SELECT id FROM {table_name}"))?; + let mut rows = stmt.query([])?; + + while let Some(row) = rows.next()? { + let data_id: String = row.get(0)?; + let record_id = Record::get_id_from_record_type(record_type, &data_id); + + tx.execute( + " + INSERT INTO sync_outgoing(record_id, data_id, record_type, commit_time) + VALUES(:record_id, :data_id, :record_type, :commit_time) + ", + named_params! { + ":record_id": record_id, + ":data_id": data_id, + ":record_type": record_type, + ":commit_time": utils::now(), + }, + )?; + } + } + + Self::set_sync_setting_stmt(&tx)?.execute(named_params! { + ":key": "remote_url", + ":value": remote_url + })?; + + tx.commit()?; + + Ok(()) + } + + pub(crate) fn get_incoming_records(&self) -> Result> { + let con = self.get_connection()?; + + let mut stmt = con.prepare( + " + SELECT + record_id, + revision, + schema_version, + data + FROM sync_incoming + ", + )?; + let records = stmt + .query_map([], |row| { + Ok(Record { + id: row.get(0)?, + revision: row.get(1)?, + schema_version: row.get(2)?, + data: row.get(3)?, + }) + })? + .map(|i| i.unwrap()) + .collect(); + + Ok(records) + } + + pub(crate) fn set_incoming_records(&self, records: &[Record]) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + for record in records { + tx.execute( + " + INSERT OR REPLACE INTO sync_incoming(record_id, revision, schema_version, data) + VALUES(:record_id, :revision, :schema_version, :data) + ", + named_params! { + ":record_id": record.id, + ":revision": record.revision, + ":schema_version": record.schema_version, + ":data": record.data, + }, + )?; + } + + tx.commit()?; + + Ok(()) + } + + pub(crate) fn remove_incoming_records(&self, record_ids: Vec) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + for record_id in record_ids { + tx.execute( + "DELETE FROM sync_incoming WHERE record_id = :record_id", + named_params! { + ":record_id": record_id + }, + )?; + } + + tx.commit()?; + + Ok(()) + } + + pub(crate) fn commit_outgoing( + &self, + tx: &Transaction, + data_id: &str, + record_type: RecordType, + updated_fields: Option>, + ) -> Result<()> { + let record_id = Record::get_id_from_record_type(record_type, data_id); + let updated_fields = updated_fields + .map(|fields| { + let fields = fields + .iter() + .map(|field| format!("'$[#]', '{field}'")) + .collect::>() + .join(","); + format!("json_insert( + COALESCE((SELECT updated_fields_json FROM sync_outgoing WHERE record_id = :record_id), '[]'), + {fields} + )") + }) + .unwrap_or("NULL".to_string()); + tx.execute(&format!(" + INSERT OR REPLACE INTO sync_outgoing(record_id, data_id, record_type, commit_time, updated_fields_json) + VALUES( + :record_id, + :data_id, + :record_type, + :commit_time, + {updated_fields} + ) + "), + named_params! { + ":record_id": record_id, + ":data_id": data_id, + ":record_type": record_type, + ":commit_time": utils::now(), + }, + )?; + + Ok(()) + } + + fn select_sync_outgoing_changes_query(where_clauses: Vec) -> String { + let mut where_clause_str = String::new(); + if !where_clauses.is_empty() { + where_clause_str = String::from("WHERE "); + where_clause_str.push_str(where_clauses.join(" AND ").as_str()); + } + + format!( + " + SELECT + record_id, + data_id, + record_type, + commit_time, + updated_fields_json + FROM sync_outgoing + {where_clause_str} + " + ) + } + + fn sql_row_to_sync_outgoing_changes(row: &Row) -> Result { + let record_id = row.get(0)?; + let data_id = row.get(1)?; + let record_type = row.get(2)?; + let commit_time = row.get(3)?; + let updated_fields = match row.get::<_, Option>(4)? { + Some(fields) => Some(serde_json::from_str(&fields)?), + None => None, + }; + + Ok(SyncOutgoingChanges { + record_id, + data_id, + record_type, + commit_time, + updated_fields, + }) + } + + pub(crate) fn get_sync_outgoing_changes(&self) -> Result> { + let con = self.get_connection()?; + + let query = Self::select_sync_outgoing_changes_query(vec![]); + let mut stmt = con.prepare(&query)?; + let mut rows = stmt.query([])?; + + let mut outgoing_changes = vec![]; + while let Some(row) = rows.next()? { + let detail = Self::sql_row_to_sync_outgoing_changes(row)?; + outgoing_changes.push(detail); + } + + Ok(outgoing_changes) + } + + pub(crate) fn get_sync_outgoing_changes_by_id( + &self, + record_id: &str, + ) -> Result> { + let con = self.get_connection()?; + let query = + Self::select_sync_outgoing_changes_query(vec!["record_id = :record_id".to_string()]); + let mut stmt = con.prepare(&query)?; + let mut rows = stmt.query(named_params! { + ":record_id": record_id, + })?; + + if let Some(row) = rows.next()? { + return Ok(Some(Self::sql_row_to_sync_outgoing_changes(row)?)); + } + + Ok(None) + } + + pub(crate) fn remove_sync_outgoing_changes(&self, record_ids: Vec) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + for record_id in record_ids { + tx.execute( + "DELETE FROM sync_outgoing WHERE record_id = :record_id", + named_params! { + ":record_id": record_id + }, + )?; + } + + tx.commit()?; + + Ok(()) + } + + fn check_commit_update(con: &Connection, record_id: &str, last_commit_time: u32) -> Result<()> { + let query = + Self::select_sync_outgoing_changes_query(vec!["record_id = :record_id".to_string()]); + let mut stmt = con.prepare(&query)?; + let mut rows = stmt.query(named_params! { + ":record_id": record_id, + })?; + + if let Some(row) = rows.next()? { + let sync_outgoing_changes = Self::sql_row_to_sync_outgoing_changes(row)?; + + if sync_outgoing_changes.commit_time > last_commit_time { + return Err(anyhow::anyhow!("Record has been updated while pulling")); + } + } + + Ok(()) + } + + pub(crate) fn commit_incoming_swap( + &self, + swap: &Swap, + sync_state: &SyncState, + last_commit_time: Option, + ) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + if let Some(last_commit_time) = last_commit_time { + Self::check_commit_update(&tx, &sync_state.record_id, last_commit_time)?; + } + + match swap { + Swap::Receive(receive_swap) => { + Self::insert_or_update_receive_swap_inner(&tx, receive_swap) + } + Swap::Send(send_swap) => Self::insert_or_update_send_swap_inner(&tx, send_swap), + Swap::Chain(chain_swap) => Self::insert_or_update_chain_swap_inner(&tx, chain_swap), + }?; + + Self::set_sync_state_stmt(&tx)?.execute(named_params! { + ":data_id": &sync_state.data_id, + ":record_id": &sync_state.record_id, + ":record_revision": &sync_state.record_revision, + ":is_local": &sync_state.is_local, + })?; + + tx.commit()?; + + Ok(()) + } + + pub(crate) fn commit_incoming_address_index( + &self, + new_address_index: u32, + sync_state: &SyncState, + last_commit_time: Option, + ) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + if let Some(last_commit_time) = last_commit_time { + Self::check_commit_update( + &tx, + &Record::get_id_from_record_type( + RecordType::LastDerivationIndex, + LAST_DERIVATION_INDEX_DATA_ID, + ), + last_commit_time, + )?; + } + + Self::update_cached_item_inner( + &tx, + KEY_LAST_DERIVATION_INDEX, + new_address_index.to_string(), + )?; + + Self::set_sync_state_stmt(&tx)?.execute(named_params! { + ":data_id": sync_state.data_id, + ":record_id": sync_state.record_id, + ":record_revision": sync_state.record_revision, + ":is_local": sync_state.is_local, + })?; + + tx.commit()?; + + Ok(()) + } + + pub(crate) fn commit_incoming_payment_details( + &self, + payment_tx_details: PaymentTxDetails, + sync_state: &SyncState, + last_commit_time: Option, + ) -> Result<()> { + let mut con = self.get_connection()?; + let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; + + if let Some(last_commit_time) = last_commit_time { + Self::check_commit_update(&tx, &sync_state.record_id, last_commit_time)?; + } + + Self::insert_or_update_payment_details_inner(&tx, &payment_tx_details, false)?; + + Self::set_sync_state_stmt(&tx)?.execute(named_params! { + ":data_id": &sync_state.data_id, + ":record_id": &sync_state.record_id, + ":record_revision": &sync_state.record_revision, + ":is_local": &sync_state.is_local, + })?; + + tx.commit()?; + + Ok(()) + } +} diff --git a/lib/core/src/receive_swap.rs b/lib/core/src/receive_swap.rs index b3edf66dc..4309c9d17 100644 --- a/lib/core/src/receive_swap.rs +++ b/lib/core/src/receive_swap.rs @@ -9,9 +9,7 @@ use lwk_wollet::hashes::hex::DisplayHex; use tokio::sync::{broadcast, Mutex}; use crate::chain::liquid::LiquidChainService; -use crate::model::PaymentState::{ - Complete, Created, Failed, Pending, RefundPending, Refundable, TimedOut, -}; +use crate::model::PaymentState::*; use crate::model::{Config, PaymentTxData, PaymentType, ReceiveSwap}; use crate::prelude::{Swap, Transaction}; use crate::{ensure_sdk, utils}; @@ -61,36 +59,45 @@ impl ReceiveSwapHandler { /// Handles status updates from Boltz for Receive swaps pub(crate) async fn on_new_status(&self, update: &boltz::Update) -> Result<()> { let id = &update.id; - let swap_state = &update.status; - let receive_swap = self - .persister - .fetch_receive_swap_by_id(id)? - .ok_or(anyhow!("No ongoing Receive Swap found for ID {id}"))?; + let status = &update.status; + let swap_state = RevSwapStates::from_str(status) + .map_err(|_| anyhow!("Invalid RevSwapState for Receive Swap {id}: {status}"))?; + let receive_swap = self.fetch_receive_swap_by_id(id)?; info!("Handling Receive Swap transition to {swap_state:?} for swap {id}"); - match RevSwapStates::from_str(swap_state) { - Ok( - RevSwapStates::SwapExpired - | RevSwapStates::InvoiceExpired - | RevSwapStates::TransactionFailed - | RevSwapStates::TransactionRefunded, - ) => { + if let Some(sync_state) = self.persister.get_sync_state_by_data_id(&receive_swap.id)? { + if !sync_state.is_local { + match swap_state { + // If the swap is not local (pulled from real-time sync) we do not claim twice + RevSwapStates::TransactionMempool | RevSwapStates::TransactionConfirmed => { + log::debug!("Received {swap_state:?} for non-local Receive swap {id} from status stream, skipping update."); + return Ok(()); + } + _ => {} + } + } + } + + match swap_state { + RevSwapStates::SwapExpired + | RevSwapStates::InvoiceExpired + | RevSwapStates::TransactionFailed + | RevSwapStates::TransactionRefunded => { match receive_swap.mrh_tx_id { Some(mrh_tx_id) => { warn!("Swap {id} is expired but MRH payment was received: txid {mrh_tx_id}") } None => { error!("Swap {id} entered into an unrecoverable state: {swap_state:?}"); - self.update_swap_info(id, Failed, None, None, None, None) - .await?; + self.update_swap_info(id, Failed, None, None, None, None)?; } } Ok(()) } // The lockup tx is in the mempool and we accept 0-conf => try to claim // Execute 0-conf preconditions check - Ok(RevSwapStates::TransactionMempool) => { + RevSwapStates::TransactionMempool => { let Some(transaction) = update.transaction.clone() else { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; @@ -122,8 +129,7 @@ impl ReceiveSwapHandler { info!("swapper lockup was verified"); let lockup_tx_id = &transaction.id; - self.update_swap_info(id, Pending, None, Some(lockup_tx_id), None, None) - .await?; + self.update_swap_info(id, Pending, None, Some(lockup_tx_id), None, None)?; let lockup_tx = utils::deserialize_tx_hex(&transaction.hex)?; @@ -173,7 +179,7 @@ impl ReceiveSwapHandler { Ok(()) } - Ok(RevSwapStates::TransactionConfirmed) => { + RevSwapStates::TransactionConfirmed => { let Some(transaction) = update.transaction.clone() else { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; @@ -203,8 +209,7 @@ impl ReceiveSwapHandler { warn!("Claim tx for Receive Swap {id} was already broadcast: txid {claim_tx_id}") } None => { - self.update_swap_info(&receive_swap.id, Pending, None, None, None, None) - .await?; + self.update_swap_info(&receive_swap.id, Pending, None, None, None, None)?; match self.claim(id).await { Ok(_) => {} Err(err) => match err { @@ -219,19 +224,39 @@ impl ReceiveSwapHandler { Ok(()) } - Ok(_) => { - debug!("Unhandled state for Receive Swap {id}: {swap_state}"); + _ => { + debug!("Unhandled state for Receive Swap {id}: {swap_state:?}"); Ok(()) } + } + } + + fn fetch_receive_swap_by_id(&self, swap_id: &str) -> Result { + self.persister + .fetch_receive_swap_by_id(swap_id) + .map_err(|_| PaymentError::PersistError)? + .ok_or(PaymentError::Generic { + err: format!("Receive Swap not found {swap_id}"), + }) + } - _ => Err(anyhow!( - "Invalid RevSwapState for Receive Swap {id}: {swap_state}" - )), + // Updates the swap without state transition validation + pub(crate) fn update_swap(&self, updated_swap: ReceiveSwap) -> Result<(), PaymentError> { + let swap = self.fetch_receive_swap_by_id(&updated_swap.id)?; + if updated_swap != swap { + info!( + "Updating Receive swap {} to {:?} (claim_tx_id = {:?}, lockup_tx_id = {:?}, mrh_tx_id = {:?})", + updated_swap.id, updated_swap.state, updated_swap.claim_tx_id, updated_swap.lockup_tx_id, updated_swap.mrh_tx_id + ); + self.persister + .insert_or_update_receive_swap(&updated_swap)?; + let _ = self.subscription_notifier.send(updated_swap.id); } + Ok(()) } - /// Transitions a Receive swap to a new state - pub(crate) async fn update_swap_info( + // Updates the swap state with validation + pub(crate) fn update_swap_info( &self, swap_id: &str, to_state: PaymentState, @@ -244,20 +269,7 @@ impl ReceiveSwapHandler { "Transitioning Receive swap {} to {:?} (claim_tx_id = {:?}, lockup_tx_id = {:?}, mrh_tx_id = {:?})", swap_id, to_state, claim_tx_id, lockup_tx_id, mrh_tx_id ); - - let swap = self - .persister - .fetch_receive_swap_by_id(swap_id) - .map_err(|_| PaymentError::PersistError)? - .ok_or(PaymentError::Generic { - err: format!("Receive Swap not found {swap_id}"), - })?; - let payment_id = claim_tx_id - .or(lockup_tx_id) - .or(mrh_tx_id) - .map(|id| id.to_string()) - .or(swap.claim_tx_id); - + let swap = self.fetch_receive_swap_by_id(swap_id)?; Self::validate_state_transition(swap.state, to_state)?; self.persister.try_handle_receive_swap_update( swap_id, @@ -267,18 +279,20 @@ impl ReceiveSwapHandler { mrh_tx_id, mrh_amount_sat, )?; + let updated_swap = self.fetch_receive_swap_by_id(swap_id)?; + + if mrh_tx_id.is_some() { + self.persister.delete_reserved_address(&swap.mrh_address)?; + } - if let Some(payment_id) = payment_id { - let _ = self.subscription_notifier.send(payment_id); + if updated_swap != swap { + let _ = self.subscription_notifier.send(updated_swap.id); } Ok(()) } async fn claim(&self, swap_id: &str) -> Result<(), PaymentError> { - let swap = self - .persister - .fetch_receive_swap_by_id(swap_id)? - .ok_or(anyhow!("No Receive Swap found for ID {swap_id}"))?; + let swap = self.fetch_receive_swap_by_id(swap_id)?; ensure_sdk!(swap.claim_tx_id.is_none(), PaymentError::AlreadyClaimed); info!("Initiating claim for Receive Swap {swap_id}"); @@ -325,19 +339,14 @@ impl ReceiveSwapHandler { is_confirmed: false, }, None, - None, + false, )?; info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}"); - self.update_swap_info( - swap_id, - Pending, - Some(&claim_tx_id), - None, - None, - None, - ) - .await + // The claim_tx_id is already set by set_receive_swap_claim_tx_id. Manually trigger notifying + // subscribers as update_swap_info will not recognise a change to the swap + _ = self.subscription_notifier.send(claim_tx_id); + Ok(()) } Err(err) => { // Multiple attempts to broadcast have failed. Unset the swap claim_tx_id @@ -428,27 +437,23 @@ impl ReceiveSwapHandler { #[cfg(test)] mod tests { - use std::{ - collections::{HashMap, HashSet}, - sync::Arc, - }; + use std::collections::{HashMap, HashSet}; use anyhow::Result; use crate::{ model::PaymentState::{self, *}, test_utils::{ - persist::{new_persister, new_receive_swap}, + persist::{create_persister, new_receive_swap}, receive_swap::new_receive_swap_handler, }, }; #[tokio::test] async fn test_receive_swap_state_transitions() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; - let storage = Arc::new(storage); + create_persister!(persister); - let receive_swap_state_handler = new_receive_swap_handler(storage.clone())?; + let receive_swap_state_handler = new_receive_swap_handler(persister.clone())?; // Test valid combinations of states let valid_combinations = HashMap::from([ @@ -467,11 +472,10 @@ mod tests { for (first_state, allowed_states) in valid_combinations.iter() { for allowed_state in allowed_states { let receive_swap = new_receive_swap(Some(*first_state)); - storage.insert_receive_swap(&receive_swap)?; + persister.insert_or_update_receive_swap(&receive_swap)?; assert!(receive_swap_state_handler .update_swap_info(&receive_swap.id, *allowed_state, None, None, None, None) - .await .is_ok()); } } @@ -491,11 +495,10 @@ mod tests { for (first_state, disallowed_states) in invalid_combinations.iter() { for disallowed_state in disallowed_states { let receive_swap = new_receive_swap(Some(*first_state)); - storage.insert_receive_swap(&receive_swap)?; + persister.insert_or_update_receive_swap(&receive_swap)?; assert!(receive_swap_state_handler .update_swap_info(&receive_swap.id, *disallowed_state, None, None, None, None) - .await .is_err()); } } diff --git a/lib/core/src/recover/mod.rs b/lib/core/src/recover/mod.rs new file mode 100644 index 000000000..7730f2a92 --- /dev/null +++ b/lib/core/src/recover/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod model; +pub(crate) mod recoverer; diff --git a/lib/core/src/recover/model.rs b/lib/core/src/recover/model.rs new file mode 100644 index 000000000..bec768e4a --- /dev/null +++ b/lib/core/src/recover/model.rs @@ -0,0 +1,741 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use anyhow::anyhow; +use boltz_client::ElementsAddress; +use electrum_client::GetBalanceRes; +use lwk_wollet::elements::Txid; +use lwk_wollet::History; +use lwk_wollet::WalletTx; + +use crate::prelude::*; + +pub(crate) type BtcScript = lwk_wollet::bitcoin::ScriptBuf; +pub(crate) type LBtcScript = lwk_wollet::elements::Script; +pub(crate) type SendSwapHistory = Vec; + +#[derive(Clone, Debug)] +pub(crate) struct HistoryTxId { + pub txid: Txid, + /// Confirmation height of txid + /// + /// -1 means unconfirmed with unconfirmed parents + /// 0 means unconfirmed with confirmed parents + pub height: i32, +} +impl HistoryTxId { + pub(crate) fn confirmed(&self) -> bool { + self.height > 0 + } +} +impl From for HistoryTxId { + fn from(value: History) -> Self { + Self::from(&value) + } +} +impl From<&History> for HistoryTxId { + fn from(value: &History) -> Self { + Self { + txid: value.txid, + height: value.height, + } + } +} + +/// A map of all our known LWK onchain txs, indexed by tx ID. Essentially our own cache of the LWK txs. +pub(crate) struct TxMap { + pub(crate) outgoing_tx_map: HashMap, + pub(crate) incoming_tx_map: HashMap, +} +impl TxMap { + pub(crate) fn from_raw_tx_map(raw_tx_map: HashMap) -> Self { + let (outgoing_tx_map, incoming_tx_map): (HashMap, HashMap) = + raw_tx_map + .into_iter() + .partition(|(_, tx)| tx.balance.values().sum::() < 0); + + Self { + outgoing_tx_map, + incoming_tx_map, + } + } +} + +pub(crate) struct RecoveredOnchainDataSend { + pub(crate) lockup_tx_id: Option, + pub(crate) claim_tx_id: Option, + pub(crate) refund_tx_id: Option, +} + +impl RecoveredOnchainDataSend { + pub(crate) fn derive_partial_state(&self, is_expired: bool) -> Option { + match &self.lockup_tx_id { + Some(_) => match &self.claim_tx_id { + Some(_) => Some(PaymentState::Complete), + None => match &self.refund_tx_id { + Some(refund_tx_id) => match refund_tx_id.confirmed() { + true => Some(PaymentState::Failed), + false => Some(PaymentState::RefundPending), + }, + None => match is_expired { + true => Some(PaymentState::RefundPending), + false => Some(PaymentState::Pending), + }, + }, + }, + None => match is_expired { + true => Some(PaymentState::Failed), + // We have no onchain data to support deriving the state as the swap could + // potentially be Created or TimedOut. In this case we return None. + false => None, + }, + } + } +} + +pub(crate) struct RecoveredOnchainDataReceive { + pub(crate) lockup_tx_id: Option, + pub(crate) claim_tx_id: Option, + pub(crate) mrh_tx_id: Option, + pub(crate) mrh_amount_sat: Option, +} + +impl RecoveredOnchainDataReceive { + pub(crate) fn derive_partial_state(&self, is_expired: bool) -> Option { + match &self.lockup_tx_id { + Some(_) => match &self.claim_tx_id { + Some(claim_tx_id) => match claim_tx_id.confirmed() { + true => Some(PaymentState::Complete), + false => Some(PaymentState::Pending), + }, + None => match is_expired { + true => Some(PaymentState::Failed), + false => Some(PaymentState::Pending), + }, + }, + None => match &self.mrh_tx_id { + Some(mrh_tx_id) => match mrh_tx_id.confirmed() { + true => Some(PaymentState::Complete), + false => Some(PaymentState::Pending), + }, + // We have no onchain data to support deriving the state as the swap could + // potentially be Created. In this case we return None. + None => match is_expired { + true => Some(PaymentState::Failed), + false => None, + }, + }, + } + } +} + +pub(crate) struct RecoveredOnchainDataChainSend { + /// LBTC tx initiated by the SDK (the "user" as per Boltz), sending funds to the swap funding address. + pub(crate) lbtc_user_lockup_tx_id: Option, + /// LBTC tx initiated by the SDK to itself, in case the initial funds have to be refunded. + pub(crate) lbtc_refund_tx_id: Option, + /// BTC tx locking up funds by the swapper + pub(crate) btc_server_lockup_tx_id: Option, + /// BTC tx that claims to the final BTC destination address. The final step in a successful swap. + pub(crate) btc_claim_tx_id: Option, +} + +// TODO: We have to be careful around overwriting the RefundPending state, as this swap monitored +// after the expiration of the swap and if new funds are detected on the lockup script they are refunded. +// Perhaps we should check in the recovery the lockup balance and set accordingly. +impl RecoveredOnchainDataChainSend { + pub(crate) fn derive_partial_state(&self, is_expired: bool) -> Option { + match &self.lbtc_user_lockup_tx_id { + Some(_) => match (&self.btc_claim_tx_id, &self.lbtc_refund_tx_id) { + (Some(btc_claim_tx_id), None) => match btc_claim_tx_id.confirmed() { + true => Some(PaymentState::Complete), + false => Some(PaymentState::Pending), + }, + (None, Some(lbtc_refund_tx_id)) => match lbtc_refund_tx_id.confirmed() { + true => Some(PaymentState::Failed), + false => Some(PaymentState::RefundPending), + }, + (Some(btc_claim_tx_id), Some(lbtc_refund_tx_id)) => { + match btc_claim_tx_id.confirmed() { + true => match lbtc_refund_tx_id.confirmed() { + true => Some(PaymentState::Complete), + false => Some(PaymentState::RefundPending), + }, + false => Some(PaymentState::Pending), + } + } + (None, None) => match is_expired { + true => Some(PaymentState::RefundPending), + false => Some(PaymentState::Pending), + }, + }, + None => match is_expired { + true => Some(PaymentState::Failed), + // We have no onchain data to support deriving the state as the swap could + // potentially be Created or TimedOut. In this case we return None. + false => None, + }, + } + } +} + +pub(crate) struct RecoveredOnchainDataChainReceive { + /// LBTC tx locking up funds by the swapper + pub(crate) lbtc_server_lockup_tx_id: Option, + /// LBTC tx that claims to our wallet. The final step in a successful swap. + pub(crate) lbtc_claim_tx_id: Option, + /// LBTC tx out address for the claim tx. + pub(crate) lbtc_claim_address: Option, + /// BTC tx initiated by the payer (the "user" as per Boltz), sending funds to the swap funding address. + pub(crate) btc_user_lockup_tx_id: Option, + /// BTC total funds available at the swap funding address. + pub(crate) btc_user_lockup_amount_sat: u64, + /// BTC tx initiated by the SDK to a user-chosen address, in case the initial funds have to be refunded. + pub(crate) btc_refund_tx_id: Option, +} + +impl RecoveredOnchainDataChainReceive { + pub(crate) fn derive_partial_state( + &self, + min_lockup_amount_sat: u64, + is_expired: bool, + ) -> Option { + let is_refundable = self.btc_user_lockup_amount_sat > 0 + && (is_expired || self.btc_user_lockup_amount_sat < min_lockup_amount_sat); + match &self.btc_user_lockup_tx_id { + Some(_) => match (&self.lbtc_claim_tx_id, &self.btc_refund_tx_id) { + (Some(lbtc_claim_tx_id), None) => match lbtc_claim_tx_id.confirmed() { + true => match is_refundable { + true => Some(PaymentState::Refundable), + false => Some(PaymentState::Complete), + }, + false => Some(PaymentState::Pending), + }, + (None, Some(btc_refund_tx_id)) => match btc_refund_tx_id.confirmed() { + true => match is_refundable { + true => Some(PaymentState::Refundable), + false => Some(PaymentState::Failed), + }, + false => Some(PaymentState::RefundPending), + }, + (Some(lbtc_claim_tx_id), Some(btc_refund_tx_id)) => { + match lbtc_claim_tx_id.confirmed() { + true => match btc_refund_tx_id.confirmed() { + true => match is_refundable { + true => Some(PaymentState::Refundable), + false => Some(PaymentState::Complete), + }, + false => Some(PaymentState::RefundPending), + }, + false => Some(PaymentState::Pending), + } + } + (None, None) => match is_refundable { + true => Some(PaymentState::Refundable), + false => Some(PaymentState::Pending), + }, + }, + None => match is_expired { + true => Some(PaymentState::Failed), + // We have no onchain data to support deriving the state as the swap could + // potentially be Created. In this case we return None. + false => None, + }, + } + } +} + +#[derive(Clone)] +pub(crate) struct SendSwapImmutableData { + pub(crate) swap_id: String, + pub(crate) lockup_script: LBtcScript, +} + +impl TryFrom for SendSwapImmutableData { + type Error = anyhow::Error; + + fn try_from(swap: SendSwap) -> std::result::Result { + let swap_script = swap.get_swap_script()?; + + let funding_address = swap_script.funding_addrs.ok_or(anyhow!( + "No funding address found for Send Swap {}", + swap.id + ))?; + + let swap_id = swap.id; + Ok(SendSwapImmutableData { + swap_id, + lockup_script: funding_address.script_pubkey(), + }) + } +} + +#[derive(Clone)] +pub(crate) struct ReceiveSwapImmutableData { + pub(crate) swap_id: String, + pub(crate) swap_timestamp: u32, + pub(crate) timeout_block_height: u32, + pub(crate) claim_script: LBtcScript, + pub(crate) mrh_script: Option, +} + +impl TryFrom for ReceiveSwapImmutableData { + type Error = anyhow::Error; + + fn try_from(swap: ReceiveSwap) -> std::result::Result { + let swap_script = swap.get_swap_script()?; + let create_response = swap.get_boltz_create_response()?; + let mrh_address = ElementsAddress::from_str(&swap.mrh_address).ok(); + + let funding_address = swap_script.funding_addrs.ok_or(anyhow!( + "No funding address found for Receive Swap {}", + swap.id + ))?; + + let swap_id = swap.id; + Ok(ReceiveSwapImmutableData { + swap_id, + swap_timestamp: swap.created_at, + timeout_block_height: create_response.timeout_block_height, + claim_script: funding_address.script_pubkey(), + mrh_script: mrh_address.map(|s| s.script_pubkey()), + }) + } +} + +pub(crate) struct ReceiveSwapHistory { + pub(crate) lbtc_claim_script_history: Vec, + pub(crate) lbtc_mrh_script_history: Vec, +} + +#[derive(Clone)] +pub(crate) struct SendChainSwapImmutableData { + swap_id: String, + lockup_script: LBtcScript, + pub(crate) claim_script: BtcScript, +} + +impl TryFrom for SendChainSwapImmutableData { + type Error = anyhow::Error; + + fn try_from(swap: ChainSwap) -> std::result::Result { + if swap.direction == Direction::Incoming { + return Err(anyhow!( + "Cannot convert incoming chain swap to `SendChainSwapImmutableData`" + )); + } + + let lockup_swap_script = swap.get_lockup_swap_script()?.as_liquid_script()?; + let claim_swap_script = swap.get_claim_swap_script()?.as_bitcoin_script()?; + + let maybe_lockup_script = lockup_swap_script + .clone() + .funding_addrs + .map(|addr| addr.script_pubkey()); + let maybe_claim_script = claim_swap_script + .clone() + .funding_addrs + .map(|addr| addr.script_pubkey()); + + let swap_id = swap.id; + match (maybe_lockup_script, maybe_claim_script) { + (Some(lockup_script), Some(claim_script)) => Ok(SendChainSwapImmutableData { + swap_id, + lockup_script, + claim_script, + }), + (lockup_script, claim_script) => Err(anyhow!("Failed to get lockup or claim script for swap {swap_id}. Lockup script: {lockup_script:?}. Claim script: {claim_script:?}")), + } + } +} + +pub(crate) struct SendChainSwapHistory { + pub(crate) lbtc_lockup_script_history: Vec, + pub(crate) btc_claim_script_history: Vec, + pub(crate) btc_claim_script_txs: Vec, +} + +#[derive(Clone)] +pub(crate) struct ReceiveChainSwapImmutableData { + swap_id: String, + pub(crate) lockup_script: BtcScript, + claim_script: LBtcScript, +} + +impl TryFrom for ReceiveChainSwapImmutableData { + type Error = anyhow::Error; + + fn try_from(swap: ChainSwap) -> std::result::Result { + if swap.direction == Direction::Outgoing { + return Err(anyhow!( + "Cannot convert outgoing chain swap to `ReceiveChainSwapImmutableData`" + )); + } + + let lockup_swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?; + let claim_swap_script = swap.get_claim_swap_script()?.as_liquid_script()?; + + let maybe_lockup_script = lockup_swap_script + .clone() + .funding_addrs + .map(|addr| addr.script_pubkey()); + let maybe_claim_script = claim_swap_script + .clone() + .funding_addrs + .map(|addr| addr.script_pubkey()); + + let swap_id = swap.id; + match (maybe_lockup_script, maybe_claim_script) { + (Some(lockup_script), Some(claim_script)) => Ok(ReceiveChainSwapImmutableData { + swap_id, + lockup_script, + claim_script, + }), + (lockup_script, claim_script) => Err(anyhow!("Failed to get lockup or claim script for swap {swap_id}. Lockup script: {lockup_script:?}. Claim script: {claim_script:?}")), + } + } +} + +pub(crate) struct ReceiveChainSwapHistory { + pub(crate) lbtc_claim_script_history: Vec, + pub(crate) btc_lockup_script_history: Vec, + pub(crate) btc_lockup_script_txs: Vec, + pub(crate) btc_lockup_script_balance: Option, +} + +/// Swap immutable data +#[derive(Default)] +pub(crate) struct SwapsList { + pub(crate) send_swap_immutable_data_by_swap_id: HashMap, + pub(crate) receive_swap_immutable_data_by_swap_id: HashMap, + pub(crate) send_chain_swap_immutable_data_by_swap_id: + HashMap, + pub(crate) receive_chain_swap_immutable_data_by_swap_id: + HashMap, +} + +impl TryFrom> for SwapsList { + type Error = anyhow::Error; + + fn try_from(swaps: Vec) -> std::result::Result { + let mut swaps_list = Self::default(); + + for swap in swaps.into_iter() { + let swap_id = swap.id(); + match swap { + Swap::Send(send_swap) => match send_swap.try_into() { + Ok(send_swap_immutable_data) => { + swaps_list + .send_swap_immutable_data_by_swap_id + .insert(swap_id, send_swap_immutable_data); + } + Err(e) => { + log::error!("Could not retrieve send swap immutable data: {e:?}"); + continue; + } + }, + Swap::Receive(receive_swap) => match receive_swap.try_into() { + Ok(receive_swap_immutable_data) => { + swaps_list + .receive_swap_immutable_data_by_swap_id + .insert(swap_id, receive_swap_immutable_data); + } + Err(e) => { + log::error!("Could not retrieve receive swap immutable data: {e:?}"); + continue; + } + }, + Swap::Chain(chain_swap) => match chain_swap.direction { + Direction::Incoming => match chain_swap.try_into() { + Ok(receive_chain_swap_immutable_data) => { + swaps_list + .receive_chain_swap_immutable_data_by_swap_id + .insert(swap_id, receive_chain_swap_immutable_data); + } + Err(e) => { + log::error!( + "Could not retrieve incoming chain swap immutable data: {e:?}" + ); + continue; + } + }, + Direction::Outgoing => match chain_swap.try_into() { + Ok(send_chain_swap_immutable_data) => { + swaps_list + .send_chain_swap_immutable_data_by_swap_id + .insert(swap_id, send_chain_swap_immutable_data); + } + Err(e) => { + log::error!( + "Could not retrieve outgoing chain swap immutable data: {e:?}" + ); + continue; + } + }, + }, + } + } + + Ok(swaps_list) + } +} + +impl SwapsList { + fn send_swaps_by_script(&self) -> HashMap { + self.send_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| (imm.lockup_script.clone(), imm)) + .collect() + } + + pub(crate) fn send_histories_by_swap_id( + &self, + lbtc_script_to_history_map: &HashMap>, + ) -> HashMap { + let send_swaps_by_script = self.send_swaps_by_script(); + + let mut data: HashMap = HashMap::new(); + lbtc_script_to_history_map + .iter() + .for_each(|(lbtc_script, lbtc_script_history)| { + if let Some(imm) = send_swaps_by_script.get(lbtc_script) { + data.insert(imm.swap_id.clone(), lbtc_script_history.clone()); + } + }); + data + } + + fn receive_swaps_by_claim_script(&self) -> HashMap { + self.receive_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| (imm.claim_script.clone(), imm)) + .collect() + } + + fn receive_swaps_by_mrh_script(&self) -> HashMap { + self.receive_swap_immutable_data_by_swap_id + .clone() + .into_values() + .filter_map(|imm| imm.mrh_script.clone().map(|mrh_script| (mrh_script, imm))) + .collect() + } + + pub(crate) fn receive_histories_by_swap_id( + &self, + lbtc_script_to_history_map: &HashMap>, + ) -> HashMap { + let receive_swaps_by_claim_script = self.receive_swaps_by_claim_script(); + let receive_swaps_by_mrh_script = self.receive_swaps_by_mrh_script(); + + let mut data: HashMap = HashMap::new(); + lbtc_script_to_history_map + .iter() + .for_each(|(lbtc_script, lbtc_script_history)| { + if let Some(imm) = receive_swaps_by_claim_script.get(lbtc_script) { + // The MRH script history filtered by the swap timeout block height + let mrh_script_history = imm + .mrh_script + .clone() + .and_then(|mrh_script| { + lbtc_script_to_history_map.get(&mrh_script).map(|h| { + h.iter() + .filter(|&tx_history| { + tx_history.height < imm.timeout_block_height as i32 + }) + .cloned() + .collect::>() + }) + }) + .unwrap_or_default(); + data.insert( + imm.swap_id.clone(), + ReceiveSwapHistory { + lbtc_claim_script_history: lbtc_script_history.clone(), + lbtc_mrh_script_history: mrh_script_history, + }, + ); + } + if let Some(imm) = receive_swaps_by_mrh_script.get(lbtc_script) { + let claim_script_history = lbtc_script_to_history_map + .get(&imm.claim_script) + .cloned() + .unwrap_or_default(); + // The MRH script history filtered by the swap timeout block height + let mrh_script_history = lbtc_script_history + .iter() + .filter(|&tx_history| tx_history.height < imm.timeout_block_height as i32) + .cloned() + .collect::>(); + data.insert( + imm.swap_id.clone(), + ReceiveSwapHistory { + lbtc_claim_script_history: claim_script_history, + lbtc_mrh_script_history: mrh_script_history, + }, + ); + } + }); + data + } + + fn send_chain_swaps_by_lbtc_lockup_script( + &self, + ) -> HashMap { + self.send_chain_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| (imm.lockup_script.clone(), imm)) + .collect() + } + + pub(crate) fn send_chain_histories_by_swap_id( + &self, + lbtc_script_to_history_map: &HashMap>, + btc_script_to_history_map: &HashMap>, + btc_script_to_txs_map: &HashMap>, + ) -> HashMap { + let send_chain_swaps_by_lbtc_script = self.send_chain_swaps_by_lbtc_lockup_script(); + + let mut data: HashMap = HashMap::new(); + lbtc_script_to_history_map + .iter() + .for_each(|(lbtc_lockup_script, lbtc_script_history)| { + if let Some(imm) = send_chain_swaps_by_lbtc_script.get(lbtc_lockup_script) { + let btc_script_history = btc_script_to_history_map + .get(&imm.claim_script) + .cloned() + .unwrap_or_default(); + let btc_script_txs = btc_script_to_txs_map + .get(&imm.claim_script) + .cloned() + .unwrap_or_default(); + + data.insert( + imm.swap_id.clone(), + SendChainSwapHistory { + lbtc_lockup_script_history: lbtc_script_history.clone(), + btc_claim_script_history: btc_script_history, + btc_claim_script_txs: btc_script_txs, + }, + ); + } + }); + data + } + + fn receive_chain_swaps_by_lbtc_claim_script( + &self, + ) -> HashMap { + self.receive_chain_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| (imm.claim_script.clone(), imm)) + .collect() + } + + pub(super) fn receive_chain_histories_by_swap_id( + &self, + lbtc_script_to_history_map: &HashMap>, + btc_script_to_history_map: &HashMap>, + btc_script_to_txs_map: &HashMap>, + btc_script_to_balance_map: &HashMap, + ) -> HashMap { + let receive_chain_swaps_by_lbtc_script = self.receive_chain_swaps_by_lbtc_claim_script(); + + let mut data: HashMap = HashMap::new(); + lbtc_script_to_history_map + .iter() + .for_each(|(lbtc_script_pk, lbtc_script_history)| { + if let Some(imm) = receive_chain_swaps_by_lbtc_script.get(lbtc_script_pk) { + let btc_script_history = btc_script_to_history_map + .get(&imm.lockup_script) + .cloned() + .unwrap_or_default(); + let btc_script_txs = btc_script_to_txs_map + .get(&imm.lockup_script) + .cloned() + .unwrap_or_default(); + let btc_script_balance = + btc_script_to_balance_map.get(&imm.lockup_script).cloned(); + + data.insert( + imm.swap_id.clone(), + ReceiveChainSwapHistory { + lbtc_claim_script_history: lbtc_script_history.clone(), + btc_lockup_script_history: btc_script_history, + btc_lockup_script_txs: btc_script_txs, + btc_lockup_script_balance: btc_script_balance, + }, + ); + } + }); + data + } + + pub(crate) fn get_swap_lbtc_scripts(&self) -> Vec { + let receive_swap_lbtc_mrh_scripts: Vec = self + .receive_swap_immutable_data_by_swap_id + .clone() + .into_values() + .filter_map(|imm| imm.mrh_script) + .collect(); + let receive_swap_lbtc_claim_scripts: Vec = self + .receive_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| imm.claim_script) + .collect(); + let send_swap_scripts: Vec = self + .send_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| imm.lockup_script) + .collect(); + let send_chain_swap_lbtc_lockup_scripts: Vec = self + .send_chain_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| imm.lockup_script) + .collect(); + let receive_chain_swap_lbtc_claim_scripts: Vec = self + .receive_chain_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| imm.claim_script) + .collect(); + let mut swap_scripts = receive_swap_lbtc_mrh_scripts.clone(); + swap_scripts.extend(receive_swap_lbtc_claim_scripts.clone()); + swap_scripts.extend(send_swap_scripts.clone()); + swap_scripts.extend(send_chain_swap_lbtc_lockup_scripts.clone()); + swap_scripts.extend(receive_chain_swap_lbtc_claim_scripts.clone()); + swap_scripts + } + + pub(crate) fn get_swap_btc_scripts(&self) -> Vec { + let mut swap_scripts = vec![]; + let send_chain_swap_btc_claim_scripts: Vec = self + .send_chain_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| imm.claim_script) + .collect(); + let receive_chain_swap_btc_lockup_scripts: Vec = self + .receive_chain_swap_immutable_data_by_swap_id + .clone() + .into_values() + .map(|imm| imm.lockup_script) + .collect(); + swap_scripts.extend(send_chain_swap_btc_claim_scripts.clone()); + swap_scripts.extend(receive_chain_swap_btc_lockup_scripts.clone()); + swap_scripts + } +} + +pub(crate) struct SwapsHistories { + pub(crate) send: HashMap, + pub(crate) receive: HashMap, + pub(crate) send_chain: HashMap, + pub(crate) receive_chain: HashMap, +} diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs new file mode 100644 index 000000000..9efc3c681 --- /dev/null +++ b/lib/core/src/recover/recoverer.rs @@ -0,0 +1,734 @@ +use std::{collections::HashMap, sync::Arc}; + +use anyhow::{anyhow, ensure, Result}; +use boltz_client::{ElementsAddress, ToHex as _}; +use electrum_client::GetBalanceRes; +use log::{debug, error, warn}; +use lwk_wollet::bitcoin::Witness; +use lwk_wollet::elements::{secp256k1_zkp, AddressParams, Txid}; +use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey; +use lwk_wollet::hashes::hex::{DisplayHex, FromHex}; +use lwk_wollet::hashes::{sha256, Hash as _}; +use lwk_wollet::WalletTx; +use tokio::sync::Mutex; + +use crate::prelude::{Direction, Swap}; +use crate::wallet::OnchainWallet; +use crate::{ + chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService}, + recover::model::{BtcScript, HistoryTxId, LBtcScript}, +}; + +use super::model::*; + +pub(crate) struct Recoverer { + master_blinding_key: MasterBlindingKey, + onchain_wallet: Arc, + liquid_chain_service: Arc>, + bitcoin_chain_service: Arc>, +} + +impl Recoverer { + pub(crate) fn new( + master_blinding_key: Vec, + onchain_wallet: Arc, + liquid_chain_service: Arc>, + bitcoin_chain_service: Arc>, + ) -> Result { + Ok(Self { + master_blinding_key: MasterBlindingKey::from_hex( + &master_blinding_key.to_lower_hex_string(), + )?, + onchain_wallet, + liquid_chain_service, + bitcoin_chain_service, + }) + } + + async fn recover_preimages<'a>( + &self, + claim_tx_ids_by_swap_id: HashMap<&'a String, Txid>, + ) -> Result> { + let claim_tx_ids: Vec = claim_tx_ids_by_swap_id.values().copied().collect(); + + let claim_txs = self + .liquid_chain_service + .lock() + .await + .get_transactions(claim_tx_ids.as_slice()) + .await + .map_err(|e| anyhow!("Failed to fetch claim txs from recovery: {e}"))?; + + let claim_tx_ids_len = claim_tx_ids.len(); + let claim_txs_len = claim_txs.len(); + ensure!( + claim_tx_ids_len == claim_txs_len, + anyhow!("Got {claim_txs_len} send claim transactions, expected {claim_tx_ids_len}") + ); + + let claim_txs_by_swap_id: HashMap<&String, lwk_wollet::elements::Transaction> = + claim_tx_ids_by_swap_id.into_keys().zip(claim_txs).collect(); + + let mut preimages = HashMap::new(); + for (swap_id, claim_tx) in claim_txs_by_swap_id { + if let Ok(preimage) = Self::get_send_swap_preimage_from_claim_tx(swap_id, &claim_tx) { + preimages.insert(swap_id, preimage); + } + } + Ok(preimages) + } + + pub(crate) fn get_send_swap_preimage_from_claim_tx( + swap_id: &str, + claim_tx: &lwk_wollet::elements::Transaction, + ) -> Result { + debug!("Send Swap {swap_id} has claim tx {}", claim_tx.txid()); + + let input = claim_tx + .input + .first() + .ok_or_else(|| anyhow!("Found no input for claim tx"))?; + + let script_witness_bytes = input.clone().witness.script_witness; + log::info!("Found Send Swap {swap_id} claim tx witness: {script_witness_bytes:?}"); + let script_witness = Witness::from(script_witness_bytes); + + let preimage_bytes = script_witness + .nth(1) + .ok_or_else(|| anyhow!("Claim tx witness has no preimage"))?; + let preimage = sha256::Hash::from_slice(preimage_bytes) + .map_err(|e| anyhow!("Claim tx witness has invalid preimage: {e}"))?; + let preimage_hex = preimage.to_hex(); + debug!("Found Send Swap {swap_id} claim tx preimage: {preimage_hex}"); + + Ok(preimage_hex) + } + + /// For each swap, recovers data from chain services. + /// + /// The returned data include txs and the partial swap state. See [PartialSwapState::derive_partial_state]. + /// + /// The caller is expected to merge this data with any other data available, then persist the + /// reconstructed swap. + /// + /// ### Arguments + /// + /// - `tx_map`: all known onchain txs of this wallet at this time, essentially our own LWK cache. + /// - `swaps`: immutable data of the swaps for which we want to recover onchain data. + pub(crate) async fn recover_from_onchain(&self, swaps: &mut [Swap]) -> Result<()> { + self.onchain_wallet.full_scan().await?; + let tx_map = TxMap::from_raw_tx_map(self.onchain_wallet.transactions_by_tx_id().await?); + + let swaps_list = swaps.to_vec().try_into()?; + let histories = self.fetch_swaps_histories(&swaps_list).await?; + + let recovered_send_data = self.recover_send_swap_tx_ids(&tx_map, histories.send)?; + let recovered_send_with_claim_tx = recovered_send_data + .iter() + .filter_map(|(swap_id, send_data)| { + send_data + .claim_tx_id + .clone() + .map(|claim_tx_id| (swap_id, claim_tx_id.txid)) + }) + .collect::>(); + let mut recovered_preimages = self.recover_preimages(recovered_send_with_claim_tx).await?; + + let recovered_receive_data = self.recover_receive_swap_tx_ids( + &tx_map, + histories.receive, + &swaps_list.receive_swap_immutable_data_by_swap_id, + )?; + let recovered_chain_send_data = self.recover_send_chain_swap_tx_ids( + &tx_map, + histories.send_chain, + &swaps_list.send_chain_swap_immutable_data_by_swap_id, + )?; + let recovered_chain_receive_data = self.recover_receive_chain_swap_tx_ids( + &tx_map, + histories.receive_chain, + &swaps_list.receive_chain_swap_immutable_data_by_swap_id, + )?; + + let bitcoin_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32; + let liquid_height = self.liquid_chain_service.lock().await.tip().await?; + + for swap in swaps.iter_mut() { + let swap_id = &swap.id(); + match swap { + Swap::Send(send_swap) => { + let Some(recovered_data) = recovered_send_data.get(swap_id) else { + log::warn!("Could not apply recovered data for Send swap {swap_id}: recovery data not found"); + continue; + }; + let is_expired = liquid_height + >= send_swap.get_boltz_create_response()?.timeout_block_height as u32; + if let Some(new_state) = recovered_data.derive_partial_state(is_expired) { + send_swap.state = new_state; + } + send_swap.lockup_tx_id = recovered_data + .lockup_tx_id + .clone() + .map(|h| h.txid.to_string()); + send_swap.refund_tx_id = recovered_data + .refund_tx_id + .clone() + .map(|h| h.txid.to_string()); + if let Some(preimage) = recovered_preimages.remove(swap_id) { + send_swap.preimage = Some(preimage); + } + } + Swap::Receive(receive_swap) => { + let Some(recovered_data) = recovered_receive_data.get(swap_id) else { + log::warn!("Could not apply recovered data for Receive swap {swap_id}: recovery data not found"); + continue; + }; + let is_expired = liquid_height + >= receive_swap + .get_boltz_create_response()? + .timeout_block_height; + if let Some(new_state) = recovered_data.derive_partial_state(is_expired) { + receive_swap.state = new_state; + } + receive_swap.claim_tx_id = recovered_data + .claim_tx_id + .clone() + .map(|history_tx_id| history_tx_id.txid.to_string()); + receive_swap.mrh_tx_id = recovered_data + .mrh_tx_id + .clone() + .map(|history_tx_id| history_tx_id.txid.to_string()); + receive_swap.lockup_tx_id = recovered_data + .lockup_tx_id + .clone() + .map(|history_tx_id| history_tx_id.txid.to_string()); + if let Some(mrh_amount_sat) = recovered_data.mrh_amount_sat { + receive_swap.payer_amount_sat = mrh_amount_sat; + receive_swap.receiver_amount_sat = mrh_amount_sat; + } + } + Swap::Chain(chain_swap) => match chain_swap.direction { + Direction::Incoming => { + let Some(recovered_data) = recovered_chain_receive_data.get(swap_id) else { + log::warn!("Could not apply recovered data for incoming Chain swap {swap_id}: recovery data not found"); + continue; + }; + let is_expired = bitcoin_height >= chain_swap.timeout_block_height; + let min_lockup_amount_sat = chain_swap.payer_amount_sat; + if let Some(new_state) = + recovered_data.derive_partial_state(min_lockup_amount_sat, is_expired) + { + chain_swap.state = new_state; + } + chain_swap.server_lockup_tx_id = recovered_data + .lbtc_server_lockup_tx_id + .clone() + .map(|h| h.txid.to_string()); + chain_swap + .claim_address + .clone_from(&recovered_data.lbtc_claim_address); + chain_swap.user_lockup_tx_id = recovered_data + .btc_user_lockup_tx_id + .clone() + .map(|h| h.txid.to_string()); + chain_swap.claim_tx_id = recovered_data + .lbtc_claim_tx_id + .clone() + .map(|h| h.txid.to_string()); + chain_swap.refund_tx_id = recovered_data + .btc_refund_tx_id + .clone() + .map(|h| h.txid.to_string()); + } + Direction::Outgoing => { + let Some(recovered_data) = recovered_chain_send_data.get(swap_id) else { + log::warn!("Could not apply recovered data for outgoing Chain swap {swap_id}: recovery data not found"); + continue; + }; + let is_expired = liquid_height >= chain_swap.timeout_block_height; + if let Some(new_state) = recovered_data.derive_partial_state(is_expired) { + chain_swap.state = new_state; + } + chain_swap.server_lockup_tx_id = recovered_data + .btc_server_lockup_tx_id + .clone() + .map(|h| h.txid.to_string()); + chain_swap.user_lockup_tx_id = recovered_data + .lbtc_user_lockup_tx_id + .clone() + .map(|h| h.txid.to_string()); + chain_swap.claim_tx_id = recovered_data + .btc_claim_tx_id + .clone() + .map(|h| h.txid.to_string()); + chain_swap.refund_tx_id = recovered_data + .lbtc_refund_tx_id + .clone() + .map(|h| h.txid.to_string()); + } + }, + } + } + + Ok(()) + } + + /// For a given [SwapList], this fetches the script histories from the chain services + async fn fetch_swaps_histories(&self, swaps_list: &SwapsList) -> Result { + let swap_lbtc_scripts = swaps_list.get_swap_lbtc_scripts(); + let lbtc_script_histories = self + .liquid_chain_service + .lock() + .await + .get_scripts_history(&swap_lbtc_scripts.iter().collect::>()) + .await?; + let lbtc_swap_scripts_len = swap_lbtc_scripts.len(); + let lbtc_script_histories_len = lbtc_script_histories.len(); + ensure!( + lbtc_swap_scripts_len == lbtc_script_histories_len, + anyhow!("Got {lbtc_script_histories_len} L-BTC script histories, expected {lbtc_swap_scripts_len}") + ); + let lbtc_script_to_history_map: HashMap> = swap_lbtc_scripts + .into_iter() + .zip(lbtc_script_histories.into_iter()) + .map(|(k, v)| (k, v.into_iter().map(HistoryTxId::from).collect())) + .collect(); + + let bitcoin_chain_service = self.bitcoin_chain_service.lock().await; + let swap_btc_script_bufs = swaps_list.get_swap_btc_scripts(); + let swap_btc_scripts = swap_btc_script_bufs + .iter() + .map(|x| x.as_script()) + .collect::>(); + let btc_script_histories = bitcoin_chain_service.get_scripts_history(&swap_btc_scripts)?; + let btx_script_tx_ids: Vec = btc_script_histories + .iter() + .flatten() + .map(|h| h.txid.to_raw_hash()) + .map(lwk_wollet::bitcoin::Txid::from_raw_hash) + .collect::>(); + + let btc_swap_scripts_len = swap_btc_script_bufs.len(); + let btc_script_histories_len = btc_script_histories.len(); + ensure!( + btc_swap_scripts_len == btc_script_histories_len, + anyhow!("Got {btc_script_histories_len} BTC script histories, expected {btc_swap_scripts_len}") + ); + let btc_script_to_history_map: HashMap> = swap_btc_script_bufs + .clone() + .into_iter() + .zip(btc_script_histories.iter()) + .map(|(k, v)| (k, v.iter().map(HistoryTxId::from).collect())) + .collect(); + + let btc_script_txs = bitcoin_chain_service.get_transactions(&btx_script_tx_ids)?; + let btc_script_balances = bitcoin_chain_service.scripts_get_balance(&swap_btc_scripts)?; + let btc_script_to_txs_map: HashMap> = + swap_btc_script_bufs + .clone() + .into_iter() + .zip(btc_script_histories.iter()) + .map(|(script, history)| { + let relevant_tx_ids: Vec = history.iter().map(|h| h.txid).collect(); + let relevant_txs: Vec = btc_script_txs + .iter() + .filter(|&tx| relevant_tx_ids.contains(&tx.txid().to_raw_hash().into())) + .cloned() + .collect(); + + (script, relevant_txs) + }) + .collect(); + let btc_script_to_balance_map: HashMap = swap_btc_script_bufs + .into_iter() + .zip(btc_script_balances) + .collect(); + + Ok(SwapsHistories { + send: swaps_list.send_histories_by_swap_id(&lbtc_script_to_history_map), + receive: swaps_list.receive_histories_by_swap_id(&lbtc_script_to_history_map), + send_chain: swaps_list.send_chain_histories_by_swap_id( + &lbtc_script_to_history_map, + &btc_script_to_history_map, + &btc_script_to_txs_map, + ), + receive_chain: swaps_list.receive_chain_histories_by_swap_id( + &lbtc_script_to_history_map, + &btc_script_to_history_map, + &btc_script_to_txs_map, + &btc_script_to_balance_map, + ), + }) + } + + /// Reconstruct Send Swap tx IDs from the onchain data and the immutable data + fn recover_send_swap_tx_ids( + &self, + tx_map: &TxMap, + send_histories_by_swap_id: HashMap, + ) -> Result> { + let mut res: HashMap = HashMap::new(); + for (swap_id, history) in send_histories_by_swap_id { + debug!("[Recover Send] Checking swap {swap_id}"); + + // If a history tx is one of our outgoing txs, it's a lockup tx + let lockup_tx_id = history + .iter() + .find(|&tx| tx_map.outgoing_tx_map.contains_key::(&tx.txid)) + .cloned(); + if lockup_tx_id.is_none() { + error!("No lockup tx found when recovering data for Send Swap {swap_id}"); + } + + // If a history tx is one of our incoming txs, it's a refund tx + let refund_tx_id = history + .iter() + .find(|&tx| tx_map.incoming_tx_map.contains_key::(&tx.txid)) + .cloned(); + + // A history tx that is neither a known incoming or outgoing tx is a claim tx + let claim_tx_id = history + .iter() + .filter(|&tx| !tx_map.incoming_tx_map.contains_key::(&tx.txid)) + .find(|&tx| !tx_map.outgoing_tx_map.contains_key::(&tx.txid)) + .cloned(); + + res.insert( + swap_id, + RecoveredOnchainDataSend { + lockup_tx_id, + claim_tx_id, + refund_tx_id, + }, + ); + } + + Ok(res) + } + + /// Reconstruct Receive Swap tx IDs from the onchain data and the immutable data + fn recover_receive_swap_tx_ids( + &self, + tx_map: &TxMap, + receive_histories_by_swap_id: HashMap, + receive_swap_immutable_data_by_swap_id: &HashMap, + ) -> Result> { + let mut res: HashMap = HashMap::new(); + for (swap_id, history) in receive_histories_by_swap_id { + debug!("[Recover Receive] Checking swap {swap_id}"); + + // The MRH script history txs filtered by the swap timestamp + let swap_timestamp = receive_swap_immutable_data_by_swap_id + .get(&swap_id) + .map(|imm: &ReceiveSwapImmutableData| imm.swap_timestamp) + .ok_or_else(|| anyhow!("Swap timestamp not found for Receive Swap {swap_id}"))?; + let mrh_txs: HashMap = history + .lbtc_mrh_script_history + .iter() + .filter_map(|h| tx_map.incoming_tx_map.get(&h.txid)) + .filter(|tx| tx.timestamp.map(|t| t > swap_timestamp).unwrap_or(true)) + .map(|tx| (tx.txid, tx.clone())) + .collect(); + let mrh_tx_id = history + .lbtc_mrh_script_history + .iter() + .find(|&tx| mrh_txs.contains_key::(&tx.txid)) + .cloned(); + let mrh_amount_sat = mrh_tx_id + .clone() + .and_then(|h| mrh_txs.get(&h.txid)) + .map(|tx| tx.balance.values().sum::().unsigned_abs()); + + let (lockup_tx_id, claim_tx_id) = match history.lbtc_claim_script_history.len() { + // Only lockup tx available + 1 => (Some(history.lbtc_claim_script_history[0].clone()), None), + + 2 => { + let first = history.lbtc_claim_script_history[0].clone(); + let second = history.lbtc_claim_script_history[1].clone(); + + if tx_map.incoming_tx_map.contains_key::(&first.txid) { + // If the first tx is a known incoming tx, it's the claim tx and the second is the lockup + (Some(second), Some(first)) + } else if tx_map.incoming_tx_map.contains_key::(&second.txid) { + // If the second tx is a known incoming tx, it's the claim tx and the first is the lockup + (Some(first), Some(second)) + } else { + // If none of the 2 txs is the claim tx, then the txs are lockup and swapper refund + // If so, we expect them to be confirmed at different heights + let first_conf_height = first.height; + let second_conf_height = second.height; + match (first.confirmed(), second.confirmed()) { + // If they're both confirmed, the one with the lowest confirmation height is the lockup + (true, true) => match first_conf_height < second_conf_height { + true => (Some(first), None), + false => (Some(second), None), + }, + + // If only one tx is confirmed, then that is the lockup + (true, false) => (Some(first), None), + (false, true) => (Some(second), None), + + // If neither is confirmed, this is an edge-case + (false, false) => { + warn!("Found unconfirmed lockup and refund txs while recovering data for Receive Swap {swap_id}"); + (None, None) + } + } + } + } + n => { + warn!("Script history with length {n} found while recovering data for Receive Swap {swap_id}"); + (None, None) + } + }; + + // Take only the lockup_tx_id and claim_tx_id if either are set, + // otherwise take the mrh_tx_id and mrh_amount_sat + let recovered_onchain_data = match (lockup_tx_id.as_ref(), claim_tx_id.as_ref()) { + (Some(_), None) | (Some(_), Some(_)) => RecoveredOnchainDataReceive { + lockup_tx_id, + claim_tx_id, + mrh_tx_id: None, + mrh_amount_sat: None, + }, + _ => RecoveredOnchainDataReceive { + lockup_tx_id: None, + claim_tx_id: None, + mrh_tx_id, + mrh_amount_sat, + }, + }; + + res.insert(swap_id, recovered_onchain_data); + } + + Ok(res) + } + + /// Reconstruct Chain Send Swap tx IDs from the onchain data and the immutable data + fn recover_send_chain_swap_tx_ids( + &self, + tx_map: &TxMap, + chain_send_histories_by_swap_id: HashMap, + send_chain_swap_immutable_data_by_swap_id: &HashMap, + ) -> Result> { + let mut res: HashMap = HashMap::new(); + for (swap_id, history) in chain_send_histories_by_swap_id { + debug!("[Recover Chain Send] Checking swap {swap_id}"); + + // If a history tx is one of our outgoing txs, it's a lockup tx + let lbtc_user_lockup_tx_id = history + .lbtc_lockup_script_history + .iter() + .find(|&tx| tx_map.outgoing_tx_map.contains_key::(&tx.txid)) + .cloned(); + if lbtc_user_lockup_tx_id.is_none() { + error!("No lockup tx found when recovering data for Chain Send Swap {swap_id}"); + } + + // If a history tx is one of our incoming txs, it's a refund tx + let lbtc_refund_tx_id = history + .lbtc_lockup_script_history + .iter() + .find(|&tx| tx_map.incoming_tx_map.contains_key::(&tx.txid)) + .cloned(); + + let (btc_server_lockup_tx_id, btc_claim_tx_id) = match history + .btc_claim_script_history + .len() + { + // Only lockup tx available + 1 => (Some(history.btc_claim_script_history[0].clone()), None), + + 2 => { + let first_tx = history.btc_claim_script_txs[0].clone(); + let first_tx_id = history.btc_claim_script_history[0].clone(); + let second_tx_id = history.btc_claim_script_history[1].clone(); + + let btc_lockup_script = send_chain_swap_immutable_data_by_swap_id + .get(&swap_id) + .map(|imm| imm.claim_script.clone()) + .ok_or_else(|| { + anyhow!("BTC claim script not found for Onchain Send Swap {swap_id}") + })?; + + // We check the full tx, to determine if this is the BTC lockup tx + let is_first_tx_lockup_tx = first_tx + .output + .iter() + .any(|out| matches!(&out.script_pubkey, x if x == &btc_lockup_script)); + + match is_first_tx_lockup_tx { + true => (Some(first_tx_id), Some(second_tx_id)), + false => (Some(second_tx_id), Some(first_tx_id)), + } + } + n => { + warn!("BTC script history with length {n} found while recovering data for Chain Send Swap {swap_id}"); + (None, None) + } + }; + + res.insert( + swap_id, + RecoveredOnchainDataChainSend { + lbtc_user_lockup_tx_id, + lbtc_refund_tx_id, + btc_server_lockup_tx_id, + btc_claim_tx_id, + }, + ); + } + + Ok(res) + } + + /// Reconstruct Chain Receive Swap tx IDs from the onchain data and the immutable data data + fn recover_receive_chain_swap_tx_ids( + &self, + tx_map: &TxMap, + chain_receive_histories_by_swap_id: HashMap, + receive_chain_swap_immutable_data_by_swap_id: &HashMap< + String, + ReceiveChainSwapImmutableData, + >, + ) -> Result> { + let secp = secp256k1_zkp::Secp256k1::new(); + + let mut res: HashMap = HashMap::new(); + for (swap_id, history) in chain_receive_histories_by_swap_id { + debug!("[Recover Chain Receive] Checking swap {swap_id}"); + + let (lbtc_server_lockup_tx_id, lbtc_claim_tx_id, lbtc_claim_address) = match history + .lbtc_claim_script_history + .len() + { + // Only lockup tx available + 1 => ( + Some(history.lbtc_claim_script_history[0].clone()), + None, + None, + ), + + 2 => { + let first = &history.lbtc_claim_script_history[0]; + let second = &history.lbtc_claim_script_history[1]; + + // If a history tx is a known incoming tx, it's the claim tx + let (lockup_tx_id, claim_tx_id) = + match tx_map.incoming_tx_map.contains_key::(&first.txid) { + true => (second, first), + false => (first, second), + }; + + // Get the claim address from the claim tx output + let claim_address = tx_map + .incoming_tx_map + .get(&claim_tx_id.txid) + .and_then(|tx| { + tx.outputs + .iter() + .find(|output| output.is_some()) + .and_then(|output| output.clone().map(|o| o.script_pubkey)) + }) + .and_then(|script| { + ElementsAddress::from_script( + &script, + Some(self.master_blinding_key.blinding_key(&secp, &script)), + &AddressParams::LIQUID, + ) + .map(|addr| addr.to_string()) + }); + + ( + Some(lockup_tx_id.clone()), + Some(claim_tx_id.clone()), + claim_address, + ) + } + n => { + warn!("L-BTC script history with length {n} found while recovering data for Chain Receive Swap {swap_id}"); + (None, None, None) + } + }; + + // Get the current confirmed amount available for the lockup script + let btc_user_lockup_amount_sat = history + .btc_lockup_script_balance + .map(|balance| balance.confirmed) + .unwrap_or_default(); + + // The btc_lockup_script_history can contain 3 kinds of txs, of which only 2 are expected: + // - 1) btc_user_lockup_tx_id (initial BTC funds sent by the sender) + // - 2A) btc_server_claim_tx_id (the swapper tx that claims the BTC funds, in success case) + // - 2B) btc_refund_tx_id (refund tx we initiate, in failure case or with lockup address reuse) + // The exact type of the second is found in the next step. + let btc_lockup_script = receive_chain_swap_immutable_data_by_swap_id + .get(&swap_id) + .map(|imm| imm.lockup_script.clone()) + .ok_or_else(|| { + anyhow!("BTC lockup script not found for Onchain Receive Swap {swap_id}") + })?; + let (btc_lockup_incoming_txs, btc_lockup_outgoing_txs): (Vec<_>, Vec<_>) = + history.btc_lockup_script_txs.iter().partition(|tx| { + tx.output + .iter() + .any(|out| matches!(&out.script_pubkey, x if x == &btc_lockup_script)) + }); + // Get the user lockup tx from the first incoming txs + let btc_user_lockup_tx_id = btc_lockup_incoming_txs + .first() + .and_then(|tx| { + history + .btc_lockup_script_history + .iter() + .find(|h| h.txid.as_raw_hash() == tx.txid().as_raw_hash()) + }) + .cloned(); + let btc_outgoing_tx_ids: Vec = btc_lockup_outgoing_txs + .iter() + .filter_map(|tx| { + history + .btc_lockup_script_history + .iter() + .find(|h| h.txid.as_raw_hash() == tx.txid().as_raw_hash()) + }) + .cloned() + .collect(); + // Get the last unconfirmed tx from the outgoing txs, else take the last outgoing tx + let btc_last_outgoing_tx_id = btc_outgoing_tx_ids + .iter() + .rev() + .find(|h| h.height == 0) + .or(btc_outgoing_tx_ids.last()) + .cloned(); + + // The first outgoing BTC tx is only a refund in case we didn't claim. + // If we claimed, then the first tx was the swapper BTC claim tx. + // If there are more than 1 outgoing txs then this is a refund from lockup address re-use, + // so take the last unconfirmed tx else take the last confirmed tx. + let btc_refund_tx_id = match lbtc_claim_tx_id.is_some() { + true => match btc_lockup_outgoing_txs.len() > 1 { + true => btc_last_outgoing_tx_id, + false => None, + }, + false => btc_last_outgoing_tx_id, + }; + + res.insert( + swap_id, + RecoveredOnchainDataChainReceive { + lbtc_server_lockup_tx_id, + lbtc_claim_tx_id, + lbtc_claim_address, + btc_user_lockup_tx_id, + btc_user_lockup_amount_sat, + btc_refund_tx_id, + }, + ); + } + + Ok(res) + } +} diff --git a/lib/core/src/restore.rs b/lib/core/src/restore.rs deleted file mode 100644 index d3f51170a..000000000 --- a/lib/core/src/restore.rs +++ /dev/null @@ -1,1056 +0,0 @@ -//! This module provides functionality for restoring the swap tx IDs from onchain data - -use std::collections::HashMap; - -use anyhow::{anyhow, Result}; -use log::{error, info}; -use lwk_wollet::elements::Txid; -use lwk_wollet::WalletTx; - -use crate::prelude::*; -use crate::restore::immutable::*; - -/// A map of all our known LWK onchain txs, indexed by tx ID. Essentially our own cache of the LWK txs. -pub(crate) struct TxMap { - outgoing_tx_map: HashMap, - incoming_tx_map: HashMap, -} -impl TxMap { - pub(crate) fn from_raw_tx_map(raw_tx_map: HashMap) -> Self { - let (outgoing_tx_map, incoming_tx_map): (HashMap, HashMap) = - raw_tx_map - .into_iter() - .partition(|(_, tx)| tx.balance.values().sum::() < 0); - - Self { - outgoing_tx_map, - incoming_tx_map, - } - } -} - -trait PartialSwapState { - /// Determine partial swap state, based on recovered chain data. - /// - /// This is a partial state, which means it may be incomplete because it's based on partial - /// information. Some swap states cannot be determined based only on chain data. - /// - /// For example, it cannot distinguish between [PaymentState::Created] and [PaymentState::TimedOut], - /// and in some cases, between [PaymentState::Created] and [PaymentState::Failed]. - fn derive_partial_state(&self) -> PaymentState; -} - -pub(crate) struct RecoveredOnchainDataSend { - lockup_tx_id: Option, - claim_tx_id: Option, - refund_tx_id: Option, -} -impl PartialSwapState for RecoveredOnchainDataSend { - fn derive_partial_state(&self) -> PaymentState { - match &self.lockup_tx_id { - Some(_) => match &self.claim_tx_id { - Some(_) => PaymentState::Complete, - None => match &self.refund_tx_id { - Some(refund_tx_id) => match refund_tx_id.confirmed() { - true => PaymentState::Failed, - false => PaymentState::RefundPending, - }, - None => PaymentState::Pending, - }, - }, - // For Send swaps, no lockup could mean both Created or TimedOut. - // However, we're in Created for a very short period of time in the originating instance, - // after which we expect Pending or TimedOut. Therefore here we default to TimedOut. - None => PaymentState::TimedOut, - } - } -} - -pub(crate) struct RecoveredOnchainDataReceive { - lockup_tx_id: Option, - claim_tx_id: Option, -} -impl PartialSwapState for RecoveredOnchainDataReceive { - fn derive_partial_state(&self) -> PaymentState { - match (&self.lockup_tx_id, &self.claim_tx_id) { - (Some(_), Some(claim_tx_id)) => match claim_tx_id.confirmed() { - true => PaymentState::Complete, - false => PaymentState::Pending, - }, - (Some(_), None) => PaymentState::Pending, - // TODO How to distinguish between Failed and Created (if in both cases, no lockup or claim tx present) - // See https://docs.boltz.exchange/v/api/lifecycle#reverse-submarine-swaps - _ => PaymentState::Created, - } - } -} - -pub(crate) struct RecoveredOnchainDataChainSend { - /// LBTC tx initiated by the SDK (the "user" as per Boltz), sending funds to the swap funding address. - lbtc_user_lockup_tx_id: Option, - /// LBTC tx initiated by the SDK to itself, in case the initial funds have to be refunded. - lbtc_refund_tx_id: Option, - /// BTC tx locking up funds by the swapper - btc_server_lockup_tx_id: Option, - /// BTC tx that claims to the final BTC destination address. The final step in a successful swap. - btc_claim_tx_id: Option, -} -impl PartialSwapState for RecoveredOnchainDataChainSend { - fn derive_partial_state(&self) -> PaymentState { - match &self.lbtc_user_lockup_tx_id { - Some(_) => match &self.btc_claim_tx_id { - Some(btc_claim_tx_id) => match btc_claim_tx_id.confirmed() { - true => PaymentState::Complete, - false => PaymentState::Pending, - }, - None => match &self.lbtc_refund_tx_id { - Some(tx) => match tx.confirmed() { - true => PaymentState::Failed, - false => PaymentState::RefundPending, - }, - None => PaymentState::Created, - }, - }, - // For Send swaps, no lockup could mean both Created or TimedOut. - // However, we're in Created for a very short period of time in the originating instance, - // after which we expect Pending or TimedOut. Therefore here we default to TimedOut. - None => PaymentState::TimedOut, - } - } -} - -pub(crate) struct RecoveredOnchainDataChainReceive { - /// LBTC tx locking up funds by the swapper - lbtc_server_lockup_tx_id: Option, - /// LBTC tx that claims to our wallet. The final step in a successful swap. - lbtc_server_claim_tx_id: Option, - /// BTC tx initiated by the payer (the "user" as per Boltz), sending funds to the swap funding address. - btc_user_lockup_tx_id: Option, - /// BTC tx initiated by the SDK to a user-chosen address, in case the initial funds have to be refunded. - btc_refund_tx_id: Option, -} -impl PartialSwapState for RecoveredOnchainDataChainReceive { - fn derive_partial_state(&self) -> PaymentState { - match &self.btc_user_lockup_tx_id { - Some(_) => match &self.lbtc_server_claim_tx_id { - Some(lbtc_server_claim_tx_id) => match lbtc_server_claim_tx_id.confirmed() { - true => PaymentState::Complete, - false => PaymentState::Pending, - }, - None => match &self.btc_refund_tx_id { - Some(tx) => match tx.confirmed() { - true => PaymentState::Failed, - false => PaymentState::RefundPending, - }, - None => PaymentState::Created, - }, - }, - None => PaymentState::Created, - } - } -} - -pub(crate) struct RecoveredOnchainData { - send: HashMap, - receive: HashMap, - chain_send: HashMap, - chain_receive: HashMap, -} - -impl LiquidSdk { - /// For each swap, recovers data from chain services. - /// - /// The returned data include txs and the partial swap state. See [PartialSwapState::derive_partial_state]. - /// - /// The caller is expected to merge this data with any other data available, then persist the - /// reconstructed swap. - /// - /// ### Arguments - /// - /// - `tx_map`: all known onchain txs of this wallet at this time, essentially our own LWK cache. - /// - `swaps`: immutable data of the swaps for which we want to recover onchain data. - pub(crate) async fn recover_from_onchain( - &self, - tx_map: TxMap, - swaps: SwapsList, - ) -> Result { - let histories = self.fetch_swaps_histories(&swaps).await?; - - let recovered_send_data = self - .recover_send_swap_tx_ids(&tx_map, histories.send) - .await?; - let recovered_receive_data = self - .recover_receive_swap_tx_ids(&tx_map, histories.receive) - .await?; - let recovered_chain_send_data = self - .recover_send_chain_swap_tx_ids( - &tx_map, - histories.send_chain, - &swaps.send_chain_swap_immutable_db_by_swap_id, - ) - .await?; - let recovered_chain_receive_data = self - .recover_receive_chain_swap_tx_ids( - &tx_map, - histories.receive_chain, - &swaps.receive_chain_swap_immutable_db_by_swap_id, - ) - .await?; - - Ok(RecoveredOnchainData { - send: recovered_send_data, - receive: recovered_receive_data, - chain_send: recovered_chain_send_data, - chain_receive: recovered_chain_receive_data, - }) - } - - /// Reconstruct Send Swap tx IDs from the onchain data and the immutable DB data - async fn recover_send_swap_tx_ids( - &self, - tx_map: &TxMap, - send_histories_by_swap_id: HashMap, - ) -> Result> { - let mut res: HashMap = HashMap::new(); - for (swap_id, history) in send_histories_by_swap_id { - // If a history tx is one of our outgoing txs, it's a lockup tx - let lockup_tx_id = history - .iter() - .find(|&tx| tx_map.outgoing_tx_map.contains_key::(&tx.txid)) - .cloned(); - if lockup_tx_id.is_none() { - error!("No lockup tx found when recovering data for Send Swap {swap_id}"); - } - - // If a history tx is one of our incoming txs, it's a refund tx - let refund_tx_id = history - .iter() - .find(|&tx| tx_map.incoming_tx_map.contains_key::(&tx.txid)) - .cloned(); - - // A history tx that is neither a known incoming or outgoing tx is a claim tx - let claim_tx_id = history - .iter() - .filter(|&tx| !tx_map.incoming_tx_map.contains_key::(&tx.txid)) - .find(|&tx| !tx_map.outgoing_tx_map.contains_key::(&tx.txid)) - .cloned(); - - res.insert( - swap_id, - RecoveredOnchainDataSend { - lockup_tx_id, - claim_tx_id, - refund_tx_id, - }, - ); - } - - Ok(res) - } - - /// Reconstruct Receive Swap tx IDs from the onchain data and the immutable DB data - async fn recover_receive_swap_tx_ids( - &self, - tx_map: &TxMap, - receive_histories_by_swap_id: HashMap, - ) -> Result> { - let mut res: HashMap = HashMap::new(); - for (swap_id, history) in receive_histories_by_swap_id { - let (lockup_tx_id, claim_tx_id) = match history.len() { - // Only lockup tx available - 1 => (Some(history[0].clone()), None), - - 2 => { - let first = history[0].clone(); - let second = history[1].clone(); - - if tx_map.incoming_tx_map.contains_key::(&first.txid) { - // If the first tx is a known incoming tx, it's the claim tx and the second is the lockup - (Some(second), Some(first)) - } else if tx_map.incoming_tx_map.contains_key::(&second.txid) { - // If the second tx is a known incoming tx, it's the claim tx and the first is the lockup - (Some(first), Some(second)) - } else { - // If none of the 2 txs is the claim tx, then the txs are lockup and swapper refund - // If so, we expect them to be confirmed at different heights - let first_conf_height = first.height; - let second_conf_height = second.height; - match (first.confirmed(), second.confirmed()) { - // If they're both confirmed, the one with the lowest confirmation height is the lockup - (true, true) => match first_conf_height < second_conf_height { - true => (Some(first), None), - false => (Some(second), None), - }, - - // If only one tx is confirmed, then that is the lockup - (true, false) => (Some(first), None), - (false, true) => (Some(second), None), - - // If neither is confirmed, this is an edge-case - (false, false) => { - error!("Found unconfirmed lockup and refund txs while recovering data for Receive Swap {swap_id}"); - (None, None) - } - } - } - } - n => { - error!("Script history with unexpected length {n} found while recovering data for Receive Swap {swap_id}"); - (None, None) - } - }; - - res.insert( - swap_id, - RecoveredOnchainDataReceive { - lockup_tx_id, - claim_tx_id, - }, - ); - } - - Ok(res) - } - - /// Reconstruct Chain Send Swap tx IDs from the onchain data and the immutable DB data - async fn recover_send_chain_swap_tx_ids( - &self, - tx_map: &TxMap, - chain_send_histories_by_swap_id: HashMap, - send_chain_swap_immutable_db_by_swap_id: &HashMap, - ) -> Result> { - let mut res: HashMap = HashMap::new(); - for (swap_id, history) in chain_send_histories_by_swap_id { - info!("[Recover Chain Send] Checking swap {swap_id}"); - - // If a history tx is one of our outgoing txs, it's a lockup tx - let lbtc_user_lockup_tx_id = history - .lbtc_lockup_script_history - .iter() - .find(|&tx| tx_map.outgoing_tx_map.contains_key::(&tx.txid)) - .cloned(); - if lbtc_user_lockup_tx_id.is_none() { - error!("No lockup tx found when recovering data for Chain Send Swap {swap_id}"); - } - - // If a history tx is one of our incoming txs, it's a refund tx - let lbtc_refund_tx_id = history - .lbtc_lockup_script_history - .iter() - .find(|&tx| tx_map.incoming_tx_map.contains_key::(&tx.txid)) - .cloned(); - - let (btc_server_lockup_tx_id, btc_claim_tx_id) = match history - .btc_claim_script_history - .len() - { - // Only lockup tx available - 1 => (Some(history.btc_claim_script_history[0].clone()), None), - - 2 => { - let first_tx = history.btc_claim_script_txs[0].clone(); - let first_tx_id = history.btc_claim_script_history[0].clone(); - let second_tx_id = history.btc_claim_script_history[1].clone(); - - let btc_lockup_script = send_chain_swap_immutable_db_by_swap_id - .get(&swap_id) - .map(|imm| imm.claim_script.clone()) - .ok_or_else(|| { - anyhow!("BTC claim script not found for Onchain Send Swap {swap_id}") - })?; - - // We check the full tx, to determine if this is the BTC lockup tx - let is_first_tx_lockup_tx = first_tx - .output - .iter() - .any(|out| matches!(&out.script_pubkey, x if x == &btc_lockup_script)); - - match is_first_tx_lockup_tx { - true => (Some(first_tx_id), Some(second_tx_id)), - false => (Some(second_tx_id), Some(first_tx_id)), - } - } - n => { - error!("BTC script history with unexpected length {n} found while recovering data for Chain Send Swap {swap_id}"); - (None, None) - } - }; - - res.insert( - swap_id, - RecoveredOnchainDataChainSend { - lbtc_user_lockup_tx_id, - lbtc_refund_tx_id, - btc_server_lockup_tx_id, - btc_claim_tx_id, - }, - ); - } - - Ok(res) - } - - /// Reconstruct Chain Receive Swap tx IDs from the onchain data and the immutable DB data - async fn recover_receive_chain_swap_tx_ids( - &self, - tx_map: &TxMap, - chain_receive_histories_by_swap_id: HashMap, - receive_chain_swap_immutable_db_by_swap_id: &HashMap, - ) -> Result> { - let mut res: HashMap = HashMap::new(); - for (swap_id, history) in chain_receive_histories_by_swap_id { - info!("[Recover Chain Receive] Checking swap {swap_id}"); - - let (lbtc_server_lockup_tx_id, lbtc_server_claim_tx_id) = match history - .lbtc_claim_script_history - .len() - { - // Only lockup tx available - 1 => (Some(history.lbtc_claim_script_history[0].clone()), None), - - 2 => { - let first = &history.lbtc_claim_script_history[0]; - let second = &history.lbtc_claim_script_history[1]; - - // If a history tx is a known incoming tx, it's the claim tx - let (lockup_tx_id, claim_tx_id) = - match tx_map.incoming_tx_map.contains_key::(&first.txid) { - true => (second, first), - false => (first, second), - }; - (Some(lockup_tx_id.clone()), Some(claim_tx_id.clone())) - } - n => { - error!("L-BTC script history with unexpected length {n} found while recovering data for Chain Receive Swap {swap_id}"); - (None, None) - } - }; - - // The btc_lockup_script_history can contain 3 kinds of txs, of which only 2 are expected: - // - 1) btc_user_lockup_tx_id (initial BTC funds sent by the sender) - // - 2A) btc_server_claim_tx_id (the swapper tx that claims the BTC funds, in Success case) - // - 2B) btc_refund_tx_id (refund tx we initiate, in Failure case) - // The exact type of the second is found in the next step. - let (btc_user_lockup_tx_id, btc_second_tx_id) = match history - .btc_lockup_script_history - .len() - { - // Only lockup tx available - 1 => (Some(history.btc_lockup_script_history[0].clone()), None), - - // Both txs available (lockup + claim, or lockup + refund) - // Any tx above the first two, we ignore, as that is address re-use which is not supported - n if n >= 2 => { - let first_tx = history.btc_lockup_script_txs[0].clone(); - let first_tx_id = history.btc_lockup_script_history[0].clone(); - let second_tx_id = history.btc_lockup_script_history[1].clone(); - - let btc_lockup_script = receive_chain_swap_immutable_db_by_swap_id - .get(&swap_id) - .map(|imm| imm.lockup_script.clone()) - .ok_or_else(|| { - anyhow!( - "BTC lockup script not found for Onchain Receive Swap {swap_id}" - ) - })?; - - // We check the full tx, to determine if this is the BTC lockup tx - let is_first_tx_lockup_tx = first_tx - .output - .iter() - .any(|out| matches!(&out.script_pubkey, x if x == &btc_lockup_script)); - - match is_first_tx_lockup_tx { - true => (Some(first_tx_id), Some(second_tx_id)), - false => (Some(second_tx_id), Some(first_tx_id)), - } - } - n => { - error!("BTC script history with unexpected length {n} found while recovering data for Chain Receive Swap {swap_id}"); - (None, None) - } - }; - - // The second BTC tx is only a refund in case we didn't claim. - // If we claimed, then the second BTC tx was an internal BTC server claim tx, which we're not tracking. - let btc_refund_tx_id = match lbtc_server_claim_tx_id.is_some() { - true => None, - false => btc_second_tx_id, - }; - - res.insert( - swap_id, - RecoveredOnchainDataChainReceive { - lbtc_server_lockup_tx_id, - lbtc_server_claim_tx_id, - btc_user_lockup_tx_id, - btc_refund_tx_id, - }, - ); - } - - Ok(res) - } -} - -/// Methods to simulate the immutable DB data available from real-time sync -// TODO Remove once real-time sync is integrated -pub(crate) mod immutable { - use std::collections::HashMap; - - use anyhow::{anyhow, ensure, Result}; - use boltz_client::{BtcSwapScript, LBtcSwapScript}; - use log::{error, info}; - use lwk_wollet::elements::Txid; - use lwk_wollet::History; - - use crate::prelude::*; - use crate::sdk::LiquidSdk; - - type BtcScript = lwk_wollet::bitcoin::ScriptBuf; - type LBtcScript = lwk_wollet::elements::Script; - - pub(crate) type SendSwapHistory = Vec; - pub(crate) type ReceiveSwapHistory = Vec; - - #[derive(Clone)] - pub(crate) struct HistoryTxId { - pub txid: Txid, - /// Confirmation height of txid - /// - /// -1 means unconfirmed with unconfirmed parents - /// 0 means unconfirmed with confirmed parents - pub height: i32, - } - impl HistoryTxId { - pub(crate) fn confirmed(&self) -> bool { - self.height > 0 - } - } - impl From for HistoryTxId { - fn from(value: History) -> Self { - Self::from(&value) - } - } - impl From<&History> for HistoryTxId { - fn from(value: &History) -> Self { - Self { - txid: value.txid, - height: value.height, - } - } - } - - #[derive(Clone)] - pub(crate) struct SendSwapImmutableData { - pub(crate) swap_id: String, - pub(crate) swap_script: LBtcSwapScript, - pub(crate) script: LBtcScript, - } - - #[derive(Clone)] - pub(crate) struct ReceiveSwapImmutableData { - pub(crate) swap_id: String, - pub(crate) swap_script: LBtcSwapScript, - pub(crate) script: LBtcScript, - } - - #[derive(Clone)] - pub(crate) struct SendChainSwapImmutableData { - swap_id: String, - lockup_swap_script: LBtcSwapScript, - lockup_script: LBtcScript, - claim_swap_script: BtcSwapScript, - pub(crate) claim_script: BtcScript, - } - - pub(crate) struct SendChainSwapHistory { - pub(crate) lbtc_lockup_script_history: Vec, - pub(crate) btc_claim_script_history: Vec, - pub(crate) btc_claim_script_txs: Vec, - } - - #[derive(Clone)] - pub(crate) struct ReceiveChainSwapImmutableData { - swap_id: String, - lockup_swap_script: BtcSwapScript, - pub(crate) lockup_script: BtcScript, - claim_swap_script: LBtcSwapScript, - claim_script: LBtcScript, - } - - pub(crate) struct ReceiveChainSwapHistory { - pub(crate) lbtc_claim_script_history: Vec, - pub(crate) btc_lockup_script_history: Vec, - pub(crate) btc_lockup_script_txs: Vec, - } - - /// Swap data received from the immutable DB - pub(crate) struct SwapsList { - pub(crate) send_swap_immutable_db_by_swap_id: HashMap, - pub(crate) receive_swap_immutable_db_by_swap_id_: HashMap, - pub(crate) send_chain_swap_immutable_db_by_swap_id: - HashMap, - pub(crate) receive_chain_swap_immutable_db_by_swap_id: - HashMap, - } - - impl SwapsList { - fn init( - send_swaps: Vec, - receive_swaps: Vec, - send_chain_swaps: Vec, - receive_chain_swaps: Vec, - ) -> Result { - let send_swap_immutable_db_by_swap_id: HashMap = - send_swaps - .iter() - .filter_map(|swap| match swap.get_swap_script() { - Ok(swap_script) => match &swap_script.funding_addrs { - Some(address) => Some(( - swap.id.clone(), - SendSwapImmutableData { - swap_id: swap.id.clone(), - swap_script: swap_script.clone(), - script: address.script_pubkey(), - }, - )), - None => { - error!("No funding address found for Send Swap {}", swap.id); - None - } - }, - Err(e) => { - error!("Failed to get swap script for Send Swap {}: {e}", swap.id); - None - } - }) - .collect(); - let send_swap_immutable_db_size = send_swap_immutable_db_by_swap_id.len(); - info!("Send Swap immutable DB: {send_swap_immutable_db_size} rows"); - - let receive_swap_immutable_db_by_swap_id_: HashMap = - receive_swaps - .iter() - .filter_map(|swap| { - let swap_id = &swap.id; - - let swap_script = swap - .get_swap_script() - .map_err(|e| { - error!("Failed to get swap script for Receive Swap {swap_id}: {e}") - }) - .ok()?; - - match &swap_script.funding_addrs { - Some(address) => Some(( - swap.id.clone(), - ReceiveSwapImmutableData { - swap_id: swap.id.clone(), - swap_script: swap_script.clone(), - script: address.script_pubkey(), - }, - )), - None => { - error!("No funding address found for Receive Swap {}", swap.id); - None - } - } - }) - .collect(); - let receive_swap_immutable_db_size = receive_swap_immutable_db_by_swap_id_.len(); - info!("Receive Swap immutable DB: {receive_swap_immutable_db_size} rows"); - - let send_chain_swap_immutable_db_by_swap_id: HashMap = - send_chain_swaps.iter().filter_map(|swap| { - let swap_id = &swap.id; - - let lockup_swap_script = swap.get_lockup_swap_script() - .map_err(|e| error!("Failed to get lockup swap script for swap {swap_id}: {e}")) - .map(|s| s.as_liquid_script().ok()) - .ok() - .flatten()?; - let claim_swap_script = swap.get_claim_swap_script() - .map_err(|e| error!("Failed to get claim swap script for swap {swap_id}: {e}")) - .map(|s| s.as_bitcoin_script().ok()).ok().flatten()?; - - let maybe_lockup_script = lockup_swap_script.clone().funding_addrs.map(|addr| addr.script_pubkey()); - let maybe_claim_script = claim_swap_script.clone().funding_addrs.map(|addr| addr.script_pubkey()); - - match (maybe_lockup_script, maybe_claim_script) { - (Some(lockup_script), Some(claim_script)) => { - Some((swap.id.clone(), SendChainSwapImmutableData { - swap_id: swap.id.clone(), - lockup_swap_script, - lockup_script, - claim_swap_script, - claim_script, - })) - } - (lockup_script, claim_script) => { - error!("Failed to get lockup or claim script for swap {swap_id}. Lockup script: {lockup_script:?}. Claim script: {claim_script:?}"); - None - } - } - }) - .collect(); - let send_chain_swap_immutable_db_size = send_chain_swap_immutable_db_by_swap_id.len(); - info!("Send Chain Swap immutable DB: {send_chain_swap_immutable_db_size} rows"); - - let receive_chain_swap_immutable_db_by_swap_id: HashMap = - receive_chain_swaps.iter().filter_map(|swap| { - let swap_id = &swap.id; - - let lockup_swap_script = swap.get_lockup_swap_script() - .map_err(|e| error!("Failed to get lockup swap script for swap {swap_id}: {e}")) - .map(|s| s.as_bitcoin_script().ok()).ok().flatten()?; - let claim_swap_script = swap.get_claim_swap_script() - .map_err(|e| error!("Failed to get claim swap script for swap {swap_id}: {e}")) - .map(|s| s.as_liquid_script().ok()).ok().flatten()?; - - let maybe_lockup_script = lockup_swap_script.clone().funding_addrs.map(|addr| addr.script_pubkey()); - let maybe_claim_script = claim_swap_script.clone().funding_addrs.map(|addr| addr.script_pubkey()); - - match (maybe_lockup_script, maybe_claim_script) { - (Some(lockup_script), Some(claim_script)) => { - Some((swap.id.clone(), ReceiveChainSwapImmutableData { - swap_id: swap.id.clone(), - lockup_swap_script, - lockup_script, - claim_swap_script, - claim_script, - })) - } - (lockup_script, claim_script) => { - error!("Failed to get lockup or claim script for swap {swap_id}. Lockup script: {lockup_script:?}. Claim script: {claim_script:?}"); - None - } - } - }) - .collect(); - let receive_chain_swap_immutable_db_size = - receive_chain_swap_immutable_db_by_swap_id.len(); - info!("Receive Chain Swap immutable DB: {receive_chain_swap_immutable_db_size} rows"); - - Ok(SwapsList { - send_swap_immutable_db_by_swap_id, - receive_swap_immutable_db_by_swap_id_, - send_chain_swap_immutable_db_by_swap_id, - receive_chain_swap_immutable_db_by_swap_id, - }) - } - - fn send_swaps_by_script(&self) -> HashMap { - self.send_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| (imm.script.clone(), imm)) - .collect() - } - - fn send_histories_by_swap_id( - &self, - lbtc_script_to_history_map: &HashMap>, - ) -> HashMap { - let send_swaps_by_script = self.send_swaps_by_script(); - - let mut data: HashMap = HashMap::new(); - lbtc_script_to_history_map - .iter() - .for_each(|(lbtc_script, lbtc_script_history)| { - if let Some(imm) = send_swaps_by_script.get(lbtc_script) { - data.insert(imm.swap_id.clone(), lbtc_script_history.clone()); - } - }); - data - } - - fn receive_swaps_by_script(&self) -> HashMap { - self.receive_swap_immutable_db_by_swap_id_ - .clone() - .into_values() - .map(|imm| (imm.script.clone(), imm)) - .collect() - } - - fn receive_histories_by_swap_id( - &self, - lbtc_script_to_history_map: &HashMap>, - ) -> HashMap { - let receive_swaps_by_script = self.receive_swaps_by_script(); - - let mut data: HashMap = HashMap::new(); - lbtc_script_to_history_map - .iter() - .for_each(|(lbtc_script, lbtc_script_history)| { - if let Some(imm) = receive_swaps_by_script.get(lbtc_script) { - data.insert(imm.swap_id.clone(), lbtc_script_history.clone()); - } - }); - data - } - - fn send_chain_swaps_by_lbtc_lockup_script( - &self, - ) -> HashMap { - self.send_chain_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| (imm.lockup_script.clone(), imm)) - .collect() - } - - fn send_chain_histories_by_swap_id( - &self, - lbtc_script_to_history_map: &HashMap>, - btc_script_to_history_map: &HashMap>, - btc_script_to_txs_map: &HashMap>, - ) -> HashMap { - let send_chain_swaps_by_lbtc_script = self.send_chain_swaps_by_lbtc_lockup_script(); - - let mut data: HashMap = HashMap::new(); - lbtc_script_to_history_map.iter().for_each( - |(lbtc_lockup_script, lbtc_script_history)| { - if let Some(imm) = send_chain_swaps_by_lbtc_script.get(lbtc_lockup_script) { - let btc_script_history = btc_script_to_history_map - .get(&imm.claim_script) - .cloned() - .unwrap_or_default(); - let btc_script_txs = btc_script_to_txs_map - .get(&imm.claim_script) - .cloned() - .unwrap_or_default(); - - data.insert( - imm.swap_id.clone(), - SendChainSwapHistory { - lbtc_lockup_script_history: lbtc_script_history.clone(), - btc_claim_script_history: btc_script_history, - btc_claim_script_txs: btc_script_txs, - }, - ); - } - }, - ); - data - } - - fn receive_chain_swaps_by_lbtc_claim_script( - &self, - ) -> HashMap { - self.receive_chain_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| (imm.claim_script.clone(), imm)) - .collect() - } - - fn receive_chain_histories_by_swap_id( - &self, - lbtc_script_to_history_map: &HashMap>, - btc_script_to_history_map: &HashMap>, - btc_script_to_txs_map: &HashMap>, - ) -> HashMap { - let receive_chain_swaps_by_lbtc_script = - self.receive_chain_swaps_by_lbtc_claim_script(); - - let mut data: HashMap = HashMap::new(); - lbtc_script_to_history_map - .iter() - .for_each(|(lbtc_script_pk, lbtc_script_history)| { - if let Some(imm) = receive_chain_swaps_by_lbtc_script.get(lbtc_script_pk) { - let btc_script_history = btc_script_to_history_map - .get(&imm.lockup_script) - .cloned() - .unwrap_or_default(); - let btc_script_txs = btc_script_to_txs_map - .get(&imm.lockup_script) - .cloned() - .unwrap_or_default(); - - data.insert( - imm.swap_id.clone(), - ReceiveChainSwapHistory { - lbtc_claim_script_history: lbtc_script_history.clone(), - btc_lockup_script_history: btc_script_history, - btc_lockup_script_txs: btc_script_txs, - }, - ); - } - }); - data - } - - fn get_all_swap_lbtc_scripts(&self) -> Vec { - let send_swap_scripts: Vec = self - .send_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| imm.script) - .collect(); - let receive_swap_scripts: Vec = self - .receive_swap_immutable_db_by_swap_id_ - .clone() - .into_values() - .map(|imm| imm.script) - .collect(); - let send_chain_swap_lbtc_lockup_scripts: Vec = self - .send_chain_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| imm.lockup_script) - .collect(); - let receive_chain_swap_lbtc_claim_scripts: Vec = self - .receive_chain_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| imm.claim_script) - .collect(); - - let mut swap_scripts = send_swap_scripts.clone(); - swap_scripts.extend(receive_swap_scripts.clone()); - swap_scripts.extend(send_chain_swap_lbtc_lockup_scripts.clone()); - swap_scripts.extend(receive_chain_swap_lbtc_claim_scripts.clone()); - swap_scripts - } - - fn get_all_swap_btc_scripts(&self) -> Vec { - let send_chain_swap_btc_claim_scripts: Vec = self - .send_chain_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| imm.claim_script) - .collect(); - let receive_chain_swap_btc_lockup_scripts: Vec = self - .receive_chain_swap_immutable_db_by_swap_id - .clone() - .into_values() - .map(|imm| imm.lockup_script) - .collect(); - - let mut swap_scripts = send_chain_swap_btc_claim_scripts.clone(); - swap_scripts.extend(receive_chain_swap_btc_lockup_scripts.clone()); - swap_scripts - } - } - - pub(crate) struct SwapsHistories { - pub(crate) send: HashMap, - pub(crate) receive: HashMap, - pub(crate) send_chain: HashMap, - pub(crate) receive_chain: HashMap, - } - - impl LiquidSdk { - pub(crate) async fn get_swaps_list(&self) -> Result { - let send_swaps = self.persister.list_send_swaps()?; - let receive_swaps = self.persister.list_receive_swaps()?; - let chain_swaps = self.persister.list_chain_swaps()?; - let (send_chain_swaps, receive_chain_swaps): (Vec, Vec) = - chain_swaps - .into_iter() - .partition(|swap| swap.direction == Direction::Outgoing); - - SwapsList::init( - send_swaps, - receive_swaps, - send_chain_swaps, - receive_chain_swaps, - ) - } - - /// For a given [SwapList], this fetches the script histories from the chain services - pub(crate) async fn fetch_swaps_histories( - &self, - swaps_list: &SwapsList, - ) -> Result { - let swap_lbtc_scripts = swaps_list.get_all_swap_lbtc_scripts(); - - let lbtc_script_histories = self - .liquid_chain_service - .lock() - .await - .get_scripts_history(&swap_lbtc_scripts.iter().collect::>()) - .await?; - let lbtc_swap_scripts_len = swap_lbtc_scripts.len(); - let lbtc_script_histories_len = lbtc_script_histories.len(); - ensure!( - lbtc_swap_scripts_len == lbtc_script_histories_len, - anyhow!("Got {lbtc_script_histories_len} L-BTC script histories, expected {lbtc_swap_scripts_len}") - ); - let lbtc_script_to_history_map: HashMap> = - swap_lbtc_scripts - .into_iter() - .zip(lbtc_script_histories.into_iter()) - .map(|(k, v)| (k, v.into_iter().map(HistoryTxId::from).collect())) - .collect(); - - let swap_btc_scripts = swaps_list.get_all_swap_btc_scripts(); - let btc_script_histories = self - .bitcoin_chain_service - .lock() - .await - .get_scripts_history( - &swap_btc_scripts - .iter() - .map(|x| x.as_script()) - .collect::>(), - )?; - let btx_script_tx_ids: Vec = btc_script_histories - .iter() - .flatten() - .map(|h| h.txid.to_raw_hash()) - .map(lwk_wollet::bitcoin::Txid::from_raw_hash) - .collect::>(); - - let btc_swap_scripts_len = swap_btc_scripts.len(); - let btc_script_histories_len = btc_script_histories.len(); - ensure!( - btc_swap_scripts_len == btc_script_histories_len, - anyhow!("Got {btc_script_histories_len} BTC script histories, expected {btc_swap_scripts_len}") - ); - let btc_script_to_history_map: HashMap> = swap_btc_scripts - .clone() - .into_iter() - .zip(btc_script_histories.iter()) - .map(|(k, v)| (k, v.iter().map(HistoryTxId::from).collect())) - .collect(); - - let btc_script_txs = self - .bitcoin_chain_service - .lock() - .await - .get_transactions(&btx_script_tx_ids)?; - let btc_script_to_txs_map: HashMap> = - swap_btc_scripts - .into_iter() - .zip(btc_script_histories.iter()) - .map(|(script, history)| { - let relevant_tx_ids: Vec = history.iter().map(|h| h.txid).collect(); - let relevant_txs: Vec = btc_script_txs - .iter() - .filter(|&tx| relevant_tx_ids.contains(&tx.txid().to_raw_hash().into())) - .cloned() - .collect(); - - (script, relevant_txs) - }) - .collect(); - - Ok(SwapsHistories { - send: swaps_list.send_histories_by_swap_id(&lbtc_script_to_history_map), - receive: swaps_list.receive_histories_by_swap_id(&lbtc_script_to_history_map), - send_chain: swaps_list.send_chain_histories_by_swap_id( - &lbtc_script_to_history_map, - &btc_script_to_history_map, - &btc_script_to_txs_map, - ), - receive_chain: swaps_list.receive_chain_histories_by_swap_id( - &lbtc_script_to_history_map, - &btc_script_to_history_map, - &btc_script_to_txs_map, - ), - }) - } - } -} diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 9e742b4ab..2ebe663cf 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1,3 +1,8 @@ +use std::collections::HashMap; +use std::ops::Not as _; +use std::time::Instant; +use std::{fs, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; + use anyhow::{anyhow, Result}; use boltz_client::{swaps::boltz::*, util::secrets::Preimage}; use buy::{BuyBitcoinApi, BuyBitcoinService}; @@ -9,19 +14,18 @@ use futures_util::{StreamExt, TryFutureExt}; use lnurl::auth::SdkLnurlAuthSigner; use log::{debug, error, info, warn}; use lwk_wollet::bitcoin::base64::Engine as _; -use lwk_wollet::elements::{AssetId, Txid}; +use lwk_wollet::elements::AssetId; use lwk_wollet::elements_miniscript::elements::bitcoin::bip32::Xpub; use lwk_wollet::hashes::{sha256, Hash}; use lwk_wollet::secp256k1::ThirtyTwoByteHash; -use lwk_wollet::{ElementsNetwork, WalletTx}; +use lwk_wollet::ElementsNetwork; +use persist::model::PaymentTxDetails; +use recover::recoverer::Recoverer; use sdk_common::bitcoin::hashes::hex::ToHex; use sdk_common::input_parser::InputType; use sdk_common::liquid::LiquidAddressData; use sdk_common::prelude::{FiatAPI, FiatCurrency, LnUrlPayError, LnUrlWithdrawError, Rate}; use signer::SdkSigner; -use std::collections::HashMap; -use std::time::Instant; -use std::{fs, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use tokio::sync::{watch, Mutex, RwLock}; use tokio::time::MissedTickBehavior; use tokio_stream::wrappers::BroadcastStream; @@ -48,6 +52,9 @@ use crate::{ use ::lightning::offers::invoice::Bolt12Invoice; use ::lightning::offers::offer::Offer; +use self::sync::client::BreezSyncerClient; +use self::sync::SyncService; + pub const DEFAULT_DATA_DIR: &str = ".data"; /// Number of blocks to monitor a swap after its timeout block height pub const CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS: u32 = 4320; @@ -68,8 +75,7 @@ pub struct LiquidSdk { pub(crate) event_manager: Arc, pub(crate) status_stream: Arc, pub(crate) swapper: Arc, - // TODO: Remove field if unnecessary - #[allow(dead_code)] + pub(crate) recoverer: Arc, pub(crate) liquid_chain_service: Arc>, pub(crate) bitcoin_chain_service: Arc>, pub(crate) fiat_api: Arc, @@ -77,6 +83,7 @@ pub struct LiquidSdk { pub(crate) shutdown_sender: watch::Sender<()>, pub(crate) shutdown_receiver: watch::Receiver<()>, pub(crate) send_swap_handler: SendSwapHandler, + pub(crate) sync_service: Arc, pub(crate) receive_swap_handler: ReceiveSwapHandler, pub(crate) chain_swap_handler: Arc, pub(crate) buy_bitcoin_service: Arc, @@ -170,9 +177,19 @@ impl LiquidSdk { &fingerprint_hex, )?; - let persister = Arc::new(Persister::new(&working_dir, config.network)?); + let (sync_trigger_tx, sync_trigger_rx) = tokio::sync::mpsc::channel::<()>(30); + let persister = Arc::new(Persister::new( + &working_dir, + config.network, + sync_trigger_tx, + )?); persister.init()?; + let liquid_chain_service = + Arc::new(Mutex::new(HybridLiquidChainService::new(config.clone())?)); + let bitcoin_chain_service = + Arc::new(Mutex::new(HybridBitcoinChainService::new(config.clone())?)); + let onchain_wallet = Arc::new(LiquidOnchainWallet::new( config.clone(), &cache_dir, @@ -180,6 +197,23 @@ impl LiquidSdk { signer.clone(), )?); + let recoverer = Arc::new(Recoverer::new( + signer.slip77_master_blinding_key()?, + onchain_wallet.clone(), + liquid_chain_service.clone(), + bitcoin_chain_service.clone(), + )?); + + let syncer_client = Box::new(BreezSyncerClient::new(config.breez_api_key.clone())); + let sync_service = Arc::new(SyncService::new( + config.sync_service_url.clone(), + persister.clone(), + recoverer.clone(), + signer.clone(), + syncer_client, + sync_trigger_rx, + )); + let event_manager = Arc::new(EventManager::new()); let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(()); @@ -190,11 +224,6 @@ impl LiquidSdk { let swapper = Arc::new(BoltzSwapper::new(config.clone(), cached_swapper_proxy_url)); let status_stream = Arc::::from(swapper.create_status_stream()); - let liquid_chain_service = - Arc::new(Mutex::new(HybridLiquidChainService::new(config.clone())?)); - let bitcoin_chain_service = - Arc::new(Mutex::new(HybridBitcoinChainService::new(config.clone())?)); - let send_swap_handler = SendSwapHandler::new( config.clone(), onchain_wallet.clone(), @@ -235,6 +264,7 @@ impl LiquidSdk { event_manager, status_stream: status_stream.clone(), swapper, + recoverer, bitcoin_chain_service, liquid_chain_service, fiat_api: breez_server, @@ -243,6 +273,7 @@ impl LiquidSdk { shutdown_receiver, send_swap_handler, receive_swap_handler, + sync_service, chain_swap_handler, buy_bitcoin_service, external_input_parsers, @@ -276,23 +307,6 @@ impl LiquidSdk { /// /// Internal method. Should only be used as part of [LiquidSdk::start]. async fn start_background_tasks(self: &Arc) -> SdkResult<()> { - // Periodically run sync() in the background - let sdk_clone = self.clone(); - let mut shutdown_rx_sync_loop = self.shutdown_receiver.clone(); - tokio::spawn(async move { - loop { - _ = sdk_clone.sync().await; - - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(30)) => {} - _ = shutdown_rx_sync_loop.changed() => { - info!("Received shutdown signal, exiting periodic sync loop"); - return; - } - } - } - }); - let reconnect_handler = Box::new(SwapperReconnectHandler::new( self.persister.clone(), self.status_stream.clone(), @@ -301,12 +315,12 @@ impl LiquidSdk { .clone() .start(reconnect_handler, self.shutdown_receiver.clone()) .await; - self.chain_swap_handler + self.sync_service .clone() .start(self.shutdown_receiver.clone()) - .await; + .await?; + self.track_new_blocks().await; self.track_swap_updates().await; - self.track_pending_swaps().await; Ok(()) } @@ -329,6 +343,70 @@ impl LiquidSdk { Ok(()) } + async fn track_new_blocks(self: &Arc) { + let cloned = self.clone(); + tokio::spawn(async move { + let mut current_liquid_block: u32 = 0; + let mut current_bitcoin_block: u32 = 0; + let mut shutdown_receiver = cloned.shutdown_receiver.clone(); + let mut interval = tokio::time::interval(Duration::from_secs(10)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + loop { + tokio::select! { + _ = interval.tick() => { + // Get the Liquid tip and process a new block + let liquid_tip_res = cloned.liquid_chain_service.lock().await.tip().await; + let is_new_liquid_block = match liquid_tip_res { + Ok(height) => { + debug!("Got Liquid tip: {height}"); + let is_new_liquid_block = height > current_liquid_block; + current_liquid_block = height; + is_new_liquid_block + }, + Err(e) => { + error!("Failed to fetch Liquid tip {e}"); + false + } + }; + // Get the Bitcoin tip and process a new block + let bitcoin_tip_res = cloned.bitcoin_chain_service.lock().await.tip().map(|tip| tip.height as u32); + let is_new_bitcoin_block = match bitcoin_tip_res { + Ok(height) => { + debug!("Got Bitcoin tip: {height}"); + let is_new_bitcoin_block = height > current_bitcoin_block; + current_bitcoin_block = height; + is_new_bitcoin_block + }, + Err(e) => { + error!("Failed to fetch Bitcoin tip {e}"); + false + } + }; + + // Only partial sync when there are no new Liquid or Bitcoin blocks + let partial_sync = (is_new_liquid_block || is_new_bitcoin_block).not(); + _ = cloned.sync(partial_sync).await; + + // Update swap handlers + if is_new_liquid_block { + cloned.chain_swap_handler.on_liquid_block(current_liquid_block).await; + cloned.send_swap_handler.on_liquid_block(current_liquid_block).await; + } + if is_new_bitcoin_block { + cloned.chain_swap_handler.on_bitcoin_block(current_bitcoin_block).await; + cloned.send_swap_handler.on_bitcoin_block(current_bitcoin_block).await; + } + } + + _ = shutdown_receiver.changed() => { + info!("Received shutdown signal, exiting track blocks loop"); + return; + } + } + } + }); + } + async fn track_swap_updates(self: &Arc) { let cloned = self.clone(); tokio::spawn(async move { @@ -387,31 +465,6 @@ impl LiquidSdk { }); } - async fn track_pending_swaps(self: &Arc) { - let cloned = self.clone(); - tokio::spawn(async move { - let mut shutdown_receiver = cloned.shutdown_receiver.clone(); - let mut interval = tokio::time::interval(Duration::from_secs(60)); - interval.set_missed_tick_behavior(MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = interval.tick() => { - if let Err(err) = cloned.send_swap_handler.track_refunds().await { - warn!("Could not refund expired swaps, error: {err:?}"); - } - if let Err(err) = cloned.chain_swap_handler.track_refunds_and_refundables().await { - warn!("Could not refund expired swaps, error: {err:?}"); - } - }, - _ = shutdown_receiver.changed() => { - info!("Received shutdown signal, exiting pending swaps loop"); - return; - } - } - } - }); - } - async fn notify_event_listeners(&self, e: SdkEvent) -> Result<()> { self.event_manager.notify(e).await; Ok(()) @@ -441,6 +494,7 @@ impl LiquidSdk { if let Some(id) = payment_id { match self.persister.get_payment(&id)? { Some(payment) => { + self.update_wallet_info().await?; match payment.status { Complete => { self.notify_event_listeners(SdkEvent::PaymentSucceeded { @@ -451,25 +505,42 @@ impl LiquidSdk { Pending => { match &payment.details.get_swap_id() { Some(swap_id) => match self.persister.fetch_swap_by_id(swap_id)? { - Swap::Chain(ChainSwap { claim_tx_id, .. }) - | Swap::Receive(ReceiveSwap { claim_tx_id, .. }) => { - match claim_tx_id { - Some(_) => { - // The claim tx has now been broadcast - self.notify_event_listeners( - SdkEvent::PaymentWaitingConfirmation { - details: payment, - }, - ) - .await? - } - None => { - // The lockup tx is in the mempool/confirmed - self.notify_event_listeners( - SdkEvent::PaymentPending { details: payment }, - ) - .await? - } + Swap::Chain(ChainSwap { claim_tx_id, .. }) => { + if claim_tx_id.is_some() { + // The claim tx has now been broadcast + self.notify_event_listeners( + SdkEvent::PaymentWaitingConfirmation { + details: payment, + }, + ) + .await? + } else { + // The lockup tx is in the mempool/confirmed + self.notify_event_listeners(SdkEvent::PaymentPending { + details: payment, + }) + .await? + } + } + Swap::Receive(ReceiveSwap { + claim_tx_id, + mrh_tx_id, + .. + }) => { + if claim_tx_id.is_some() || mrh_tx_id.is_some() { + // The a claim or mrh tx has now been broadcast + self.notify_event_listeners( + SdkEvent::PaymentWaitingConfirmation { + details: payment, + }, + ) + .await? + } else { + // The lockup tx is in the mempool/confirmed + self.notify_event_listeners(SdkEvent::PaymentPending { + details: payment, + }) + .await? } } Swap::Send(_) => { @@ -520,53 +591,19 @@ impl LiquidSdk { Ok(()) } - /// Get the wallet info, calculating the current pending and confirmed balances. - pub async fn get_info(&self) -> Result { + /// Get the wallet info from persistant storage + pub async fn get_info(&self) -> SdkResult { self.ensure_is_started().await?; - let mut pending_send_sat = 0; - let mut pending_receive_sat = 0; - let mut confirmed_sent_sat = 0; - let mut confirmed_received_sat = 0; - - for p in self - .list_payments(&ListPaymentsRequest { - ..Default::default() - }) - .await? - { - match p.payment_type { - PaymentType::Send => match p.status { - Complete => confirmed_sent_sat += p.amount_sat, - Failed => { - confirmed_sent_sat += p.amount_sat; - confirmed_received_sat += - p.details.get_refund_tx_amount_sat().unwrap_or_default(); - } - Pending => match p.details.get_refund_tx_amount_sat() { - Some(refund_tx_amount_sat) => { - confirmed_sent_sat += p.amount_sat; - pending_receive_sat += refund_tx_amount_sat; - } - None => pending_send_sat += p.amount_sat, - }, - Created => pending_send_sat += p.amount_sat, - Refundable | RefundPending | TimedOut => {} - }, - PaymentType::Receive => match p.status { - Complete => confirmed_received_sat += p.amount_sat, - Pending => pending_receive_sat += p.amount_sat, - Created | Refundable | RefundPending | Failed | TimedOut => {} - }, + let maybe_wallet_info = self.persister.get_wallet_info()?; + match maybe_wallet_info { + Some(wallet_info) => Ok(wallet_info), + None => { + self.update_wallet_info().await?; + self.persister.get_wallet_info()?.ok_or(SdkError::Generic { + err: "Info not found".into(), + }) } } - - Ok(GetInfoResponse { - balance_sat: confirmed_received_sat - confirmed_sent_sat - pending_send_sat, - pending_send_sat, - pending_receive_sat, - fingerprint: self.onchain_wallet.fingerprint()?, - pubkey: self.onchain_wallet.pubkey()?, - }) } /// Sign given message with the private key. Returns a zbase encoded signature. @@ -894,6 +931,12 @@ impl LiquidSdk { }; } Ok(InputType::Bolt11 { invoice }) => { + self.sync_service + .pull() + .await + .map_err(|err| PaymentError::Generic { + err: format!("Could not pull real-time sync changes: {err:?}"), + })?; self.ensure_send_is_not_self_transfer(&invoice.bolt11)?; self.validate_bolt11_invoice(&invoice.bolt11)?; @@ -1191,8 +1234,13 @@ impl LiquidSdk { self.persister.insert_or_update_payment( tx_data.clone(), - Some(destination.clone()), - description.clone(), + Some(PaymentTxDetails { + tx_id: tx_id.clone(), + destination: destination.clone(), + description: description.clone(), + ..Default::default() + }), + false, )?; self.emit_payment_updated(Some(tx_id)).await?; // Emit Pending event @@ -1233,9 +1281,13 @@ impl LiquidSdk { Some(swap) => match swap.state { Created => swap, TimedOut => { - self.send_swap_handler - .update_swap_info(&swap.id, PaymentState::Created, None, None, None) - .await?; + self.send_swap_handler.update_swap_info( + &swap.id, + PaymentState::Created, + None, + None, + None, + )?; swap } Pending => return Err(PaymentError::PaymentInProgress), @@ -1296,7 +1348,7 @@ impl LiquidSdk { state: PaymentState::Created, refund_private_key: keypair.display_secret().to_string(), }; - self.persister.insert_send_swap(&swap)?; + self.persister.insert_or_update_send_swap(&swap)?; swap } }; @@ -1307,7 +1359,7 @@ impl LiquidSdk { .try_lockup(&swap, &create_response) .await?; - self.wait_for_payment(Swap::Send(swap), create_response.accept_zero_conf) + self.wait_for_payment_with_timeout(Swap::Send(swap), create_response.accept_zero_conf) .await .map(|payment| SendPaymentResponse { payment }) } @@ -1582,15 +1634,15 @@ impl LiquidSdk { created_at: utils::now(), state: PaymentState::Created, }; - self.persister.insert_chain_swap(&swap)?; + self.persister.insert_or_update_chain_swap(&swap)?; self.status_stream.track_swap_id(&swap_id)?; - self.wait_for_payment(Swap::Chain(swap), accept_zero_conf) + self.wait_for_payment_with_timeout(Swap::Chain(swap), accept_zero_conf) .await .map(|payment| SendPaymentResponse { payment }) } - async fn wait_for_payment( + async fn wait_for_payment_with_timeout( &self, swap: Swap, accept_zero_conf: bool, @@ -1609,8 +1661,12 @@ impl LiquidSdk { None => { debug!("Timeout occurred without payment, set swap to timed out"); match swap { - Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None).await?, - Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None, None).await?, + Swap::Send(_) => self.send_swap_handler.update_swap_info(&expected_swap_id, TimedOut, None, None, None)?, + Swap::Chain(_) => self.chain_swap_handler.update_swap_info(&ChainSwapUpdate { + swap_id: expected_swap_id, + to_state: TimedOut, + ..Default::default() + })?, _ => () } return Err(PaymentError::PaymentTimeout) @@ -1639,8 +1695,8 @@ impl LiquidSdk { return Ok(payment); } }, - Ok(event) => debug!("Unhandled event: {event:?}"), - Err(e) => debug!("Received error waiting for event: {e:?}"), + Ok(event) => debug!("Unhandled event waiting for payment: {event:?}"), + Err(e) => debug!("Received error waiting for payment: {e:?}"), } } } @@ -1902,7 +1958,7 @@ impl LiquidSdk { Bolt11InvoiceDescription::Hash(_) => None, }; self.persister - .insert_receive_swap(&ReceiveSwap { + .insert_or_update_receive_swap(&ReceiveSwap { id: swap_id.clone(), preimage: preimage_str, create_response_json, @@ -1916,10 +1972,9 @@ impl LiquidSdk { PaymentError::generic(&format!("Failed to serialize ReversePair: {e:?}")) })?, claim_fees_sat: reverse_pair.fees.claim_estimate(), - claim_tx_id: None, lockup_tx_id: None, + claim_tx_id: None, mrh_address: mrh_addr_str, - mrh_script_pubkey: mrh_addr.to_unconfidential().script_pubkey().to_hex(), mrh_tx_id: None, created_at: utils::now(), state: PaymentState::Created, @@ -2021,7 +2076,7 @@ impl LiquidSdk { created_at: utils::now(), state: PaymentState::Created, }; - self.persister.insert_chain_swap(&swap)?; + self.persister.insert_or_update_chain_swap(&swap)?; self.status_stream.track_swap_id(&swap.id)?; Ok(swap) } @@ -2150,9 +2205,23 @@ impl LiquidSdk { /// (within last [CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS] blocks = ~30 days), calling this /// is not necessary as it happens automatically in the background. pub async fn rescan_onchain_swaps(&self) -> SdkResult<()> { - self.chain_swap_handler - .rescan_incoming_user_lockup_txs(true) + let mut rescannable_swaps: Vec = self + .persister + .list_chain_swaps()? + .into_iter() + .map(Into::into) + .collect(); + self.recoverer + .recover_from_onchain(&mut rescannable_swaps) .await?; + for swap in rescannable_swaps { + let swap_id = &swap.id(); + if let Swap::Chain(chain_swap) = swap { + if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) { + error!("Error persisting rescanned Chain Swap {swap_id}: {e}"); + } + } + } Ok(()) } @@ -2232,144 +2301,205 @@ impl LiquidSdk { .await?) } + pub(crate) async fn get_monitored_swaps_list(&self, partial_sync: bool) -> Result> { + let receive_swaps = self + .persister + .list_recoverable_receive_swaps()? + .into_iter() + .map(Into::into) + .collect(); + match partial_sync { + false => { + let bitcoin_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32; + let liquid_height = self.liquid_chain_service.lock().await.tip().await?; + let final_swap_states = [PaymentState::Complete, PaymentState::Failed]; + + let send_swaps = self + .persister + .list_recoverable_send_swaps()? + .into_iter() + .map(Into::into) + .collect(); + let chain_swaps: Vec = self + .persister + .list_chain_swaps()? + .into_iter() + .filter(|swap| match swap.direction { + Direction::Incoming => { + bitcoin_height + <= swap.timeout_block_height + + CHAIN_SWAP_MONITORING_PERIOD_BITCOIN_BLOCKS + } + Direction::Outgoing => { + !final_swap_states.contains(&swap.state) + && liquid_height <= swap.timeout_block_height + } + }) + .map(Into::into) + .collect(); + Ok([receive_swaps, send_swaps, chain_swaps].concat()) + } + true => Ok(receive_swaps), + } + } + /// This method fetches the chain tx data (onchain and mempool) using LWK. For every wallet tx, /// it inserts or updates a corresponding entry in our Payments table. - async fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> { - let payments_before_sync: HashMap = self - .list_payments(&ListPaymentsRequest::default()) - .await? - .into_iter() - .flat_map(|payment| { - // Index payments by both tx_id (lockup/claim) and refund_tx_id - let mut res = vec![]; - if let Some(tx_id) = payment.tx_id.clone() { - res.push((tx_id, payment.clone())); + async fn sync_payments_with_chain_data(&self, partial_sync: bool) -> Result<()> { + let mut recoverable_swaps = self.get_monitored_swaps_list(partial_sync).await?; + self.recoverer + .recover_from_onchain(&mut recoverable_swaps) + .await?; + let mut tx_map = self.onchain_wallet.transactions_by_tx_id().await?; + + for swap in recoverable_swaps { + let swap_id = &swap.id(); + + // Update the payment wallet txs before updating the swap so the tx data is pulled into the payment + match swap { + Swap::Receive(receive_swap) => { + let history_updates = vec![&receive_swap.claim_tx_id, &receive_swap.mrh_tx_id]; + for tx_id in history_updates + .into_iter() + .flatten() + .collect::>() + { + if let Some(tx) = + tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?) + { + self.persister + .insert_or_update_payment_with_wallet_tx(&tx)?; + } + } + if let Err(e) = self.receive_swap_handler.update_swap(receive_swap) { + error!("Error persisting recovered receive swap {swap_id}: {e}"); + } } - if let Some(refund_tx_id) = payment.get_refund_tx_id() { - res.push((refund_tx_id, payment)); + Swap::Send(send_swap) => { + let history_updates = vec![&send_swap.lockup_tx_id, &send_swap.refund_tx_id]; + for tx_id in history_updates + .into_iter() + .flatten() + .collect::>() + { + if let Some(tx) = + tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?) + { + self.persister + .insert_or_update_payment_with_wallet_tx(&tx)?; + } + } + if let Err(e) = self.send_swap_handler.update_swap(send_swap) { + error!("Error persisting recovered send swap {swap_id}: {e}"); + } } - res - }) - .collect(); - if with_scan { - self.onchain_wallet.full_scan().await?; + Swap::Chain(chain_swap) => { + let history_updates = match chain_swap.direction { + Direction::Incoming => vec![&chain_swap.claim_tx_id], + Direction::Outgoing => { + vec![&chain_swap.user_lockup_tx_id, &chain_swap.refund_tx_id] + } + }; + for tx_id in history_updates + .into_iter() + .flatten() + .collect::>() + { + if let Some(tx) = + tx_map.remove(&lwk_wollet::elements::Txid::from_str(tx_id)?) + { + self.persister + .insert_or_update_payment_with_wallet_tx(&tx)?; + } + } + if let Err(e) = self.chain_swap_handler.update_swap(chain_swap) { + error!("Error persisting recovered Chain Swap {swap_id}: {e}"); + } + } + }; } - let pending_receive_swaps_by_claim_tx_id = - self.persister.list_pending_receive_swaps_by_claim_tx_id()?; - let ongoing_receive_swaps_by_mrh_script_pubkey = self + let payments = self .persister - .list_ongoing_receive_swaps_by_mrh_script_pubkey()?; - let pending_send_swaps_by_refund_tx_id = - self.persister.list_pending_send_swaps_by_refund_tx_id()?; - let pending_chain_swaps_by_claim_tx_id = - self.persister.list_pending_chain_swaps_by_claim_tx_id()?; - let pending_chain_swaps_by_refund_tx_id = - self.persister.list_pending_chain_swaps_by_refund_tx_id()?; - - let tx_map: HashMap = self - .onchain_wallet - .transactions() - .await? - .iter() - .map(|tx| (tx.txid, tx.clone())) - .collect(); + .get_payments_by_tx_id(&ListPaymentsRequest::default())?; + + // We query only these that may need update, should be a fast query. + let unconfirmed_payment_txs_data = self.persister.list_unconfirmed_payment_txs_data()?; + let unconfirmed_txs_by_id: HashMap = unconfirmed_payment_txs_data + .into_iter() + .map(|tx| (tx.tx_id.clone(), tx)) + .collect::>(); for tx in tx_map.values() { let tx_id = tx.txid.to_string(); - let is_tx_confirmed = tx.height.is_some(); - let amount_sat = tx.balance.values().sum::(); - let maybe_script_pubkey = tx - .outputs - .iter() - .find(|output| output.is_some()) - .and_then(|output| output.clone().map(|o| o.script_pubkey.to_hex())); - let mrh_script_pubkey = maybe_script_pubkey.clone().unwrap_or_default(); - - self.persister.insert_or_update_payment( - PaymentTxData { - tx_id: tx_id.clone(), - timestamp: tx.timestamp, - amount_sat: amount_sat.unsigned_abs(), - fees_sat: tx.fee, - payment_type: match amount_sat >= 0 { - true => PaymentType::Receive, - false => PaymentType::Send, - }, - is_confirmed: is_tx_confirmed, - }, - maybe_script_pubkey, - None, - )?; - - if let Some(swap) = pending_receive_swaps_by_claim_tx_id.get(&tx_id) { - if is_tx_confirmed { - self.receive_swap_handler - .update_swap_info(&swap.id, Complete, None, None, None, None) - .await?; - } - } else if let Some(swap) = - ongoing_receive_swaps_by_mrh_script_pubkey.get(&mrh_script_pubkey) - { - // Update the swap status according to the MRH tx confirmation state - let to_state = match is_tx_confirmed { - true => Complete, - false => Pending, - }; - self.receive_swap_handler - .update_swap_info( - &swap.id, - to_state, - None, - None, - Some(&tx_id), - Some(amount_sat.unsigned_abs()), - ) - .await?; - // Remove the used MRH address from the reserved addresses - self.persister.delete_reserved_address(&swap.mrh_address)?; - } else if let Some(swap) = pending_send_swaps_by_refund_tx_id.get(&tx_id) { - if is_tx_confirmed { - self.send_swap_handler - .update_swap_info(&swap.id, Failed, None, None, None) - .await?; - } - } else if let Some(swap) = pending_chain_swaps_by_claim_tx_id.get(&tx_id) { - if is_tx_confirmed { - self.chain_swap_handler - .update_swap_info(&swap.id, Complete, None, None, None, None) - .await?; - } - } else if let Some(swap) = pending_chain_swaps_by_refund_tx_id.get(&tx_id) { - if is_tx_confirmed { - self.chain_swap_handler - .update_swap_info(&swap.id, Failed, None, None, None, None) - .await?; - } - } else { - // Payments that are not directly associated with a swap - match payments_before_sync.get(&tx_id) { - None => { - // A completely new payment brought in by this sync, in mempool or confirmed - // Covers events: - // - onchain Receive Pending and Complete - // - onchain Send Complete - self.emit_payment_updated(Some(tx_id)).await?; - } - Some(payment_before_sync) => { - if payment_before_sync.status == Pending && is_tx_confirmed { - // A know payment that was in the mempool, but is now confirmed - // Covers events: Send and Receive direct onchain payments transitioning to Complete - self.emit_payment_updated(Some(tx_id)).await?; - } + let maybe_payment = payments.get(&tx_id); + let mut updated = false; + match maybe_payment { + // When no payment is found or its a Liquid payment + None + | Some(Payment { + details: PaymentDetails::Liquid { .. }, + .. + }) => { + let updated_needed = maybe_payment.map_or(true, |payment| { + payment.status == Pending && tx.height.is_some() + }); + if updated_needed { + // An unknown tx which needs inserting or a known Liquid payment tx + // that was in the mempool, but is now confirmed + self.persister.insert_or_update_payment_with_wallet_tx(tx)?; + self.emit_payment_updated(Some(tx_id.clone())).await?; + updated = true } } + + _ => {} + } + if !updated && unconfirmed_txs_by_id.contains_key(&tx_id) && tx.height.is_some() { + // An unconfirmed tx that was not found in the payments table + self.persister.insert_or_update_payment_with_wallet_tx(tx)?; } } + self.update_wallet_info().await?; Ok(()) } + async fn update_wallet_info(&self) -> Result<()> { + let transactions = self.onchain_wallet.transactions().await?; + let wallet_amount_sat = transactions + .into_iter() + .map(|tx| tx.balance.values().sum::()) + .sum::(); + debug!("Onchain wallet balance: {wallet_amount_sat} sats"); + + let mut pending_send_sat = 0; + let mut pending_receive_sat = 0; + let payments = self.persister.get_payments(&ListPaymentsRequest { + states: Some(vec![PaymentState::Pending, PaymentState::RefundPending]), + ..Default::default() + })?; + + for payment in payments { + match payment.payment_type { + PaymentType::Send => match payment.details.get_refund_tx_amount_sat() { + Some(refund_tx_amount_sat) => pending_receive_sat += refund_tx_amount_sat, + None => pending_send_sat += payment.amount_sat, + }, + PaymentType::Receive => pending_receive_sat += payment.amount_sat, + } + } + + let info_response = GetInfoResponse { + balance_sat: wallet_amount_sat as u64, + pending_send_sat, + pending_receive_sat, + fingerprint: self.onchain_wallet.fingerprint()?, + pubkey: self.onchain_wallet.pubkey()?, + }; + self.persister.set_wallet_info(&info_response) + } + /// Lists the SDK payments in reverse chronological order, from newest to oldest. /// The payments are determined based on onchain transactions and swaps. pub async fn list_payments( @@ -2413,7 +2543,7 @@ impl LiquidSdk { } /// Synchronizes the local state with the mempool and onchain data. - pub async fn sync(&self) -> SdkResult<()> { + pub async fn sync(&self, partial_sync: bool) -> SdkResult<()> { self.ensure_is_started().await?; let t0 = Instant::now(); @@ -2424,16 +2554,16 @@ impl LiquidSdk { match is_first_sync { true => { self.event_manager.pause_notifications(); - self.sync_payments_with_chain_data(true).await?; + self.sync_payments_with_chain_data(partial_sync).await?; self.event_manager.resume_notifications(); self.persister.set_is_first_sync_complete(true)?; } false => { - self.sync_payments_with_chain_data(true).await?; + self.sync_payments_with_chain_data(partial_sync).await?; } } let duration_ms = Instant::now().duration_since(t0).as_millis(); - info!("Synchronized with mempool and onchain data (t = {duration_ms} ms)"); + info!("Synchronized (partial: {partial_sync}) with mempool and onchain data ({duration_ms} ms)"); self.notify_event_listeners(SdkEvent::Synced).await?; Ok(()) @@ -2522,6 +2652,8 @@ impl LiquidSdk { Ok(PrepareLnUrlPayResponse { destination: prepare_response.destination, fees_sat: prepare_response.fees_sat, + data: req.data, + comment: req.comment, success_action: data.success_action, }) } @@ -2558,42 +2690,81 @@ impl LiquidSdk { let maybe_sa_processed: Option = match prepare_response .success_action + .clone() { Some(sa) => { - let processed_sa = match sa { - // For AES, we decrypt the contents on the fly + match sa { + // For AES, we decrypt the contents if the preimage is available SuccessAction::Aes { data } => { - let PaymentDetails::Lightning { preimage, .. } = &payment.details else { + let PaymentDetails::Lightning { + swap_id, preimage, .. + } = &payment.details + else { return Err(LnUrlPayError::Generic { err: format!("Invalid payment type: expected type `PaymentDetails::Lightning`, got payment details {:?}.", payment.details), }); }; - let preimage_str = preimage.clone().ok_or(LnUrlPayError::Generic { - err: "Payment successful but no preimage found".to_string(), - })?; - let preimage = sha256::Hash::from_str(&preimage_str).map_err(|_| { - LnUrlPayError::Generic { - err: "Invalid preimage".to_string(), + match preimage { + Some(preimage_str) => { + debug!( + "Decrypting AES success action with preimage for Send Swap {}", + swap_id + ); + let preimage = + sha256::Hash::from_str(preimage_str).map_err(|_| { + LnUrlPayError::Generic { + err: "Invalid preimage".to_string(), + } + })?; + let preimage_arr: [u8; 32] = preimage.into_32(); + let result = match (data, &preimage_arr).try_into() { + Ok(data) => AesSuccessActionDataResult::Decrypted { data }, + Err(e) => AesSuccessActionDataResult::ErrorStatus { + reason: e.to_string(), + }, + }; + Some(SuccessActionProcessed::Aes { result }) } - })?; - let preimage_arr: [u8; 32] = preimage.into_32(); - let result = match (data, &preimage_arr).try_into() { - Ok(data) => AesSuccessActionDataResult::Decrypted { data }, - Err(e) => AesSuccessActionDataResult::ErrorStatus { - reason: e.to_string(), - }, - }; - SuccessActionProcessed::Aes { result } + None => { + debug!("Preimage not yet available to decrypt AES success action for Send Swap {}", swap_id); + None + } + } } - SuccessAction::Message { data } => SuccessActionProcessed::Message { data }, - SuccessAction::Url { data } => SuccessActionProcessed::Url { data }, - }; - Some(processed_sa) + SuccessAction::Message { data } => { + Some(SuccessActionProcessed::Message { data }) + } + SuccessAction::Url { data } => Some(SuccessActionProcessed::Url { data }), + } } None => None, }; + let lnurl_pay_domain = match prepare_response.data.ln_address { + Some(_) => None, + None => Some(prepare_response.data.domain), + }; + if let (Some(tx_id), Some(destination)) = + (payment.tx_id.clone(), payment.destination.clone()) + { + self.persister + .insert_or_update_payment_details(PaymentTxDetails { + tx_id, + destination, + description: prepare_response.comment.clone(), + lnurl_info: Some(LnUrlInfo { + ln_address: prepare_response.data.ln_address, + lnurl_pay_comment: prepare_response.comment, + lnurl_pay_domain, + lnurl_pay_metadata: Some(prepare_response.data.metadata_str), + lnurl_pay_success_action: maybe_sa_processed.clone(), + lnurl_pay_unprocessed_success_action: prepare_response.success_action, + lnurl_withdraw_endpoint: None, + }), + })?; + } + Ok(LnUrlPayResult::EndpointSuccess { data: model::LnUrlPaySuccessData { payment, @@ -2623,19 +2794,39 @@ impl LiquidSdk { let receive_res = self .receive_payment(&ReceivePaymentRequest { prepare_response, - description: None, + description: req.description.clone(), use_description_hash: Some(false), }) .await?; - if let Ok(invoice) = parse_invoice(&receive_res.destination) { - let res = validate_lnurl_withdraw(req.data, invoice).await?; - Ok(res) - } else { - Err(LnUrlWithdrawError::Generic { + let Ok(invoice) = parse_invoice(&receive_res.destination) else { + return Err(LnUrlWithdrawError::Generic { err: "Received unexpected output from receive request".to_string(), - }) + }); + }; + + let res = validate_lnurl_withdraw(req.data.clone(), invoice.clone()).await?; + if let LnUrlWithdrawResult::Ok { data: _ } = res { + if let Some(ReceiveSwap { + claim_tx_id: Some(tx_id), + .. + }) = self + .persister + .fetch_receive_swap_by_invoice(&invoice.bolt11)? + { + self.persister + .insert_or_update_payment_details(PaymentTxDetails { + tx_id, + destination: receive_res.destination, + description: req.description, + lnurl_info: Some(LnUrlInfo { + lnurl_withdraw_endpoint: Some(req.data.callback), + ..Default::default() + }), + })?; + } } + Ok(res) } /// Third and last step of LNURL-auth. The first step is [parse], which also validates the LNURL destination @@ -2833,7 +3024,7 @@ mod tests { test_utils::{ chain::{MockBitcoinChainService, MockHistory, MockLiquidChainService}, chain_swap::{new_chain_swap, TEST_BITCOIN_TX}, - persist::{new_persister, new_receive_swap, new_send_swap}, + persist::{create_persister, new_receive_swap, new_send_swap}, sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services}, status_stream::MockStatusStream, swapper::MockSwapper, @@ -2900,17 +3091,17 @@ mod tests { $args.accepts_zero_conf, $args.user_lockup_tx_id, ); - $persister.insert_chain_swap(&swap).unwrap(); + $persister.insert_or_update_chain_swap(&swap).unwrap(); Swap::Chain(swap) } "send" => { let swap = new_send_swap($args.initial_payment_state); - $persister.insert_send_swap(&swap).unwrap(); + $persister.insert_or_update_send_swap(&swap).unwrap(); Swap::Send(swap) } "receive" => { let swap = new_receive_swap($args.initial_payment_state); - $persister.insert_receive_swap(&swap).unwrap(); + $persister.insert_or_update_receive_swap(&swap).unwrap(); Swap::Receive(swap) } _ => panic!(), @@ -2938,8 +3129,7 @@ mod tests { #[tokio::test] async fn test_receive_swap_update_tracking() -> Result<()> { - let (_tmp_dir, persister) = new_persister()?; - let persister = Arc::new(persister); + create_persister!(persister); let swapper = Arc::new(MockSwapper::default()); let status_stream = Arc::new(MockStatusStream::new()); @@ -3005,8 +3195,7 @@ mod tests { #[tokio::test] async fn test_send_swap_update_tracking() -> Result<()> { - let (_tmp_dir, persister) = new_persister()?; - let persister = Arc::new(persister); + create_persister!(persister); let swapper = Arc::new(MockSwapper::default()); let status_stream = Arc::new(MockStatusStream::new()); @@ -3062,8 +3251,7 @@ mod tests { #[tokio::test] async fn test_chain_swap_update_tracking() -> Result<()> { - let (_tmp_dir, persister) = new_persister()?; - let persister = Arc::new(persister); + create_persister!(persister); let swapper = Arc::new(MockSwapper::default()); let status_stream = Arc::new(MockStatusStream::new()); let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); diff --git a/lib/core/src/send_swap.rs b/lib/core/src/send_swap.rs index f152ca384..e612d052d 100644 --- a/lib/core/src/send_swap.rs +++ b/lib/core/src/send_swap.rs @@ -2,20 +2,24 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::{str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; +use async_trait::async_trait; use boltz_client::swaps::boltz; use boltz_client::swaps::{boltz::CreateSubmarineResponse, boltz::SubSwapStates}; use boltz_client::util::secrets::Preimage; -use boltz_client::{Bolt11Invoice, ToHex}; +use boltz_client::Bolt11Invoice; use futures_util::TryFutureExt; use log::{debug, error, info, warn}; -use lwk_wollet::bitcoin::Witness; use lwk_wollet::elements::{LockTime, Transaction}; -use lwk_wollet::hashes::{sha256, Hash}; +use lwk_wollet::hashes::sha256; +use lwk_wollet::secp256k1::ThirtyTwoByteHash; +use sdk_common::prelude::{AesSuccessActionDataResult, SuccessAction, SuccessActionProcessed}; use tokio::sync::{broadcast, Mutex}; use crate::chain::liquid::LiquidChainService; -use crate::model::{Config, PaymentState::*, SendSwap}; +use crate::model::{BlockListener, Config, PaymentState::*, SendSwap}; +use crate::persist::model::PaymentTxDetails; use crate::prelude::{PaymentTxData, PaymentType, Swap}; +use crate::recover::recoverer::Recoverer; use crate::swapper::Swapper; use crate::wallet::OnchainWallet; use crate::{ensure_sdk, utils}; @@ -35,6 +39,17 @@ pub(crate) struct SendSwapHandler { subscription_notifier: broadcast::Sender, } +#[async_trait] +impl BlockListener for SendSwapHandler { + async fn on_bitcoin_block(&self, _height: u32) {} + + async fn on_liquid_block(&self, _height: u32) { + if let Err(err) = self.check_refunds().await { + warn!("Could not refund expired swaps, error: {err:?}"); + } + } +} + impl SendSwapHandler { pub(crate) fn new( config: Config, @@ -61,25 +76,23 @@ impl SendSwapHandler { /// Handles status updates from Boltz for Send swaps pub(crate) async fn on_new_status(&self, update: &boltz::Update) -> Result<()> { let id = &update.id; - let swap_state = &update.status; - let swap = self - .persister - .fetch_send_swap_by_id(id)? - .ok_or(anyhow!("No ongoing Send Swap found for ID {id}"))?; - + let status = &update.status; + let swap_state = SubSwapStates::from_str(status) + .map_err(|_| anyhow!("Invalid SubSwapState for Send Swap {id}: {status}"))?; + let swap = self.fetch_send_swap_by_id(id)?; info!("Handling Send Swap transition to {swap_state:?} for swap {id}"); // See https://docs.boltz.exchange/v/api/lifecycle#normal-submarine-swaps - match SubSwapStates::from_str(swap_state) { + match swap_state { // Boltz has locked the HTLC - Ok(SubSwapStates::InvoiceSet) => { + SubSwapStates::InvoiceSet => { warn!("Received `invoice.set` state for Send Swap {id}"); Ok(()) } // Boltz has detected the lockup in the mempool, we can speed up // the claim by doing so cooperatively - Ok(SubSwapStates::TransactionClaimPending) => { + SubSwapStates::TransactionClaimPending => { self.cooperate_claim(&swap).await.map_err(|e| { error!("Could not cooperate Send Swap {id} claim: {e}"); anyhow!("Could not post claim details. Err: {e:?}") @@ -89,7 +102,7 @@ impl SendSwapHandler { } // Boltz announced they successfully broadcast the (cooperative or non-cooperative) claim tx - Ok(SubSwapStates::TransactionClaimed) => { + SubSwapStates::TransactionClaimed => { debug!("Send Swap {id} has been claimed"); match swap.preimage { @@ -105,8 +118,7 @@ impl SendSwapHandler { .await?; self.validate_send_swap_preimage(id, &swap.invoice, &preimage) .await?; - self.update_swap_info(id, Complete, Some(&preimage), None, None) - .await?; + self.update_swap_info(id, Complete, Some(&preimage), None, None)?; } } @@ -118,11 +130,9 @@ impl SendSwapHandler { // 2. The swap has expired (>24h) // 3. Lockup failed (we sent too little funds) // We initiate a cooperative refund, and then fallback to a regular one - Ok( - SubSwapStates::TransactionLockupFailed - | SubSwapStates::InvoiceFailedToPay - | SubSwapStates::SwapExpired, - ) => { + SubSwapStates::TransactionLockupFailed + | SubSwapStates::InvoiceFailedToPay + | SubSwapStates::SwapExpired => { match swap.lockup_tx_id { Some(_) => match swap.refund_tx_id { Some(refund_tx_id) => warn!( @@ -146,29 +156,24 @@ impl SendSwapHandler { None, None, refund_tx_id.as_deref(), - ) - .await?; + )?; } }, // Do not attempt broadcasting a refund if lockup tx was never sent and swap is // unrecoverable. We resolve the payment as failed. None => { warn!("Send Swap {id} is in an unrecoverable state: {swap_state:?}, and lockup tx has never been broadcast. Resolving payment as failed."); - self.update_swap_info(id, Failed, None, None, None).await?; + self.update_swap_info(id, Failed, None, None, None)?; } } Ok(()) } - Ok(_) => { - debug!("Unhandled state for Send Swap {id}: {swap_state}"); + _ => { + debug!("Unhandled state for Send Swap {id}: {swap_state:?}"); Ok(()) } - - _ => Err(anyhow!( - "Invalid SubSwapState for Send Swap {id}: {swap_state}" - )), } } @@ -232,17 +237,93 @@ impl SendSwapHandler { is_confirmed: false, }, None, - None, + false, )?; - self.update_swap_info(swap_id, Pending, None, Some(&lockup_tx_id), None) - .await?; + self.update_swap_info(swap_id, Pending, None, Some(&lockup_tx_id), None)?; Ok(lockup_tx) } - /// Transitions a Send swap to a new state - pub(crate) async fn update_swap_info( + fn fetch_send_swap_by_id(&self, swap_id: &str) -> Result { + self.persister + .fetch_send_swap_by_id(swap_id) + .map_err(|_| PaymentError::PersistError)? + .ok_or(PaymentError::Generic { + err: format!("Send Swap not found {swap_id}"), + }) + } + + // Updates the swap without state transition validation + pub(crate) fn update_swap(&self, updated_swap: SendSwap) -> Result<(), PaymentError> { + let swap = self.fetch_send_swap_by_id(&updated_swap.id)?; + let lnurl_info_updated = self.update_swap_lnurl_info(&swap, &updated_swap)?; + if updated_swap != swap || lnurl_info_updated { + info!( + "Updating Send swap {} to {:?} (lockup_tx_id = {:?}, refund_tx_id = {:?})", + updated_swap.id, + updated_swap.state, + updated_swap.lockup_tx_id, + updated_swap.refund_tx_id + ); + self.persister.insert_or_update_send_swap(&updated_swap)?; + let _ = self.subscription_notifier.send(updated_swap.id); + } + Ok(()) + } + + pub(crate) fn update_swap_lnurl_info( + &self, + swap: &SendSwap, + updated_swap: &SendSwap, + ) -> Result { + if swap.preimage.is_none() { + let Some(tx_id) = updated_swap.lockup_tx_id.clone() else { + return Ok(false); + }; + let Some(ref preimage_str) = updated_swap.preimage.clone() else { + return Ok(false); + }; + if let Some(PaymentTxDetails { + destination, + description, + lnurl_info: Some(mut lnurl_info), + .. + }) = self.persister.get_payment_details(&tx_id)? + { + if let Some(SuccessAction::Aes { data }) = + lnurl_info.lnurl_pay_unprocessed_success_action.clone() + { + debug!( + "Decrypting AES success action with preimage for Send Swap {}", + swap.id + ); + let preimage = sha256::Hash::from_str(preimage_str)?; + let preimage_arr: [u8; 32] = preimage.into_32(); + let result = match (data, &preimage_arr).try_into() { + Ok(data) => AesSuccessActionDataResult::Decrypted { data }, + Err(e) => AesSuccessActionDataResult::ErrorStatus { + reason: e.to_string(), + }, + }; + lnurl_info.lnurl_pay_success_action = + Some(SuccessActionProcessed::Aes { result }); + self.persister + .insert_or_update_payment_details(PaymentTxDetails { + tx_id, + destination, + description, + lnurl_info: Some(lnurl_info), + })?; + return Ok(true); + } + } + } + Ok(false) + } + + // Updates the swap state with validation + pub(crate) fn update_swap_info( &self, swap_id: &str, to_state: PaymentState, @@ -250,17 +331,11 @@ impl SendSwapHandler { lockup_tx_id: Option<&str>, refund_tx_id: Option<&str>, ) -> Result<(), PaymentError> { - info!("Transitioning Send swap {swap_id} to {to_state:?} (lockup_tx_id = {lockup_tx_id:?}, refund_tx_id = {refund_tx_id:?})"); - - let swap: SendSwap = self - .persister - .fetch_send_swap_by_id(swap_id) - .map_err(|_| PaymentError::PersistError)? - .ok_or(PaymentError::Generic { - err: format!("Send Swap not found {swap_id}"), - })?; - let payment_id = lockup_tx_id.map(|c| c.to_string()).or(swap.lockup_tx_id); - + info!( + "Transitioning Send swap {} to {:?} (lockup_tx_id = {:?}, refund_tx_id = {:?})", + swap_id, to_state, lockup_tx_id, refund_tx_id + ); + let swap = self.fetch_send_swap_by_id(swap_id)?; Self::validate_state_transition(swap.state, to_state)?; self.persister.try_handle_send_swap_update( swap_id, @@ -269,8 +344,10 @@ impl SendSwapHandler { lockup_tx_id, refund_tx_id, )?; - if let Some(payment_id) = payment_id { - let _ = self.subscription_notifier.send(payment_id); + let updated_swap = self.fetch_send_swap_by_id(swap_id)?; + let lnurl_info_updated = self.update_swap_lnurl_info(&swap, &updated_swap)?; + if updated_swap != swap || lnurl_info_updated { + let _ = self.subscription_notifier.send(updated_swap.id); } Ok(()) } @@ -288,14 +365,13 @@ impl SendSwapHandler { Some(&claim_tx_details.preimage), None, None, - ) - .await?; + )?; self.swapper .claim_send_swap_cooperative(send_swap, claim_tx_details, &output_address)?; Ok(()) } - async fn get_preimage_from_script_path_claim_spend( + pub(crate) async fn get_preimage_from_script_path_claim_spend( &self, swap: &SendSwap, ) -> Result { @@ -330,37 +406,20 @@ impl SendSwapHandler { }), Some(claim_tx_entry) => { let claim_tx_id = claim_tx_entry.txid; - debug!("Send Swap {id} has claim tx {claim_tx_id}"); - let claim_tx = self .chain_service .lock() .await .get_transactions(&[claim_tx_id]) .await - .map_err(|e| anyhow!("Failed to fetch claim tx {claim_tx_id}: {e}"))? + .map_err(|e| anyhow!("Failed to fetch claim txs {claim_tx_id:?}: {e}"))? .first() .cloned() - .ok_or_else(|| anyhow!("Fetching claim tx returned an empty list"))?; - - let input = claim_tx - .input - .first() - .ok_or_else(|| anyhow!("Found no input for claim tx"))?; - - let script_witness_bytes = input.clone().witness.script_witness; - info!("Found Send Swap {id} claim tx witness: {script_witness_bytes:?}"); - let script_witness = Witness::from(script_witness_bytes); - - let preimage_bytes = script_witness - .nth(1) - .ok_or_else(|| anyhow!("Claim tx witness has no preimage"))?; - let preimage = sha256::Hash::from_slice(preimage_bytes) - .map_err(|e| anyhow!("Claim tx witness has invalid preimage: {e}"))?; - let preimage_hex = preimage.to_hex(); - debug!("Found Send Swap {id} claim tx preimage: {preimage_hex}"); + .ok_or(anyhow!("Claim tx not found for Send swap {id}"))?; - Ok(preimage_hex) + Ok(Recoverer::get_send_swap_preimage_from_claim_tx( + id, &claim_tx, + )?) } } } @@ -474,9 +533,8 @@ impl SendSwapHandler { }; if let Ok(refund_tx_id) = refund_tx_id_result { - let update_swap_info_result = self - .update_swap_info(&swap.id, RefundPending, None, None, Some(&refund_tx_id)) - .await; + let update_swap_info_result = + self.update_swap_info(&swap.id, RefundPending, None, None, Some(&refund_tx_id)); if let Err(err) = update_swap_info_result { warn!( "Could not update Send swap {} information, error: {err:?}", @@ -489,7 +547,7 @@ impl SendSwapHandler { // Attempts refunding all payments whose state is `RefundPending` and with no // refund_tx_id field present - pub(crate) async fn track_refunds(&self) -> Result<(), PaymentError> { + pub(crate) async fn check_refunds(&self) -> Result<(), PaymentError> { let pending_swaps = self.persister.list_pending_send_swaps()?; self.try_refund_all(&pending_swaps).await; Ok(()) @@ -551,25 +609,21 @@ impl SendSwapHandler { #[cfg(test)] mod tests { - use std::{ - collections::{HashMap, HashSet}, - sync::Arc, - }; + use std::collections::{HashMap, HashSet}; use anyhow::Result; use crate::{ model::PaymentState::{self, *}, test_utils::{ - persist::{new_persister, new_send_swap}, + persist::{create_persister, new_send_swap}, send_swap::new_send_swap_handler, }, }; #[tokio::test] async fn test_send_swap_state_transitions() -> Result<()> { - let (_temp_dir, storage) = new_persister()?; - let storage = Arc::new(storage); + create_persister!(storage); let send_swap_handler = new_send_swap_handler(storage.clone())?; // Test valid combinations of states @@ -591,11 +645,10 @@ mod tests { for (first_state, allowed_states) in valid_combinations.iter() { for allowed_state in allowed_states { let send_swap = new_send_swap(Some(*first_state)); - storage.insert_send_swap(&send_swap)?; + storage.insert_or_update_send_swap(&send_swap)?; assert!(send_swap_handler .update_swap_info(&send_swap.id, *allowed_state, None, None, None) - .await .is_ok()); } } @@ -615,11 +668,10 @@ mod tests { for (first_state, disallowed_states) in invalid_combinations.iter() { for disallowed_state in disallowed_states { let send_swap = new_send_swap(Some(*first_state)); - storage.insert_send_swap(&send_swap)?; + storage.insert_or_update_send_swap(&send_swap)?; assert!(send_swap_handler .update_swap_info(&send_swap.id, *disallowed_state, None, None, None) - .await .is_err()); } } diff --git a/lib/core/src/signer.rs b/lib/core/src/signer.rs index cbb2a622a..8b58909b2 100644 --- a/lib/core/src/signer.rs +++ b/lib/core/src/signer.rs @@ -6,7 +6,7 @@ use boltz_client::PublicKey; use lwk_common::Signer as LwkSigner; use lwk_wollet::bitcoin::bip32::Xpriv; use lwk_wollet::bitcoin::Network; -use lwk_wollet::elements_miniscript; +use lwk_wollet::elements_miniscript::{self, ToPublicKey as _}; use lwk_wollet::elements_miniscript::{ bitcoin::{self, bip32::DerivationPath}, elements::{ @@ -253,6 +253,21 @@ impl Signer for SdkSigner { .as_byte_array() .to_vec()) } + + fn ecies_encrypt(&self, msg: Vec) -> Result, SignerError> { + let keypair = self.xprv.to_keypair(&self.secp); + let rc_pub = keypair.public_key().to_public_key().to_bytes(); + ecies::encrypt(&rc_pub, &msg).map_err(|err| SignerError::Generic { + err: format!("Could not encrypt data: {err}"), + }) + } + + fn ecies_decrypt(&self, msg: Vec) -> Result, SignerError> { + let rc_prv = self.xprv.to_priv().to_bytes(); + ecies::decrypt(&rc_prv, &msg).map_err(|err| SignerError::Generic { + err: format!("Could not decrypt data: {err}"), + }) + } } #[cfg(test)] diff --git a/lib/core/src/sync/client.rs b/lib/core/src/sync/client.rs new file mode 100644 index 000000000..698b0908b --- /dev/null +++ b/lib/core/src/sync/client.rs @@ -0,0 +1,122 @@ +use std::time::Duration; + +use anyhow::{anyhow, Error, Result}; + +use async_trait::async_trait; +use log::debug; +use tokio::sync::Mutex; +use tonic::{ + metadata::{errors::InvalidMetadataValue, Ascii, MetadataValue}, + service::{interceptor::InterceptedService, Interceptor}, + transport::{Channel, ClientTlsConfig, Endpoint}, + Request, Status, +}; + +use super::model::sync::{ + syncer_client::SyncerClient as ProtoSyncerClient, ListChangesReply, ListChangesRequest, + SetRecordReply, SetRecordRequest, +}; + +#[async_trait] +pub(crate) trait SyncerClient: Send + Sync { + async fn connect(&self, connect_url: String) -> Result<()>; + async fn push(&self, req: SetRecordRequest) -> Result; + async fn pull(&self, req: ListChangesRequest) -> Result; + async fn disconnect(&self) -> Result<()>; +} + +pub(crate) struct BreezSyncerClient { + grpc_channel: Mutex>, + api_key: Option, +} + +impl BreezSyncerClient { + pub(crate) fn new(api_key: Option) -> Self { + Self { + grpc_channel: Mutex::new(None), + api_key, + } + } + + fn create_endpoint(server_url: &str) -> Result { + Ok(Endpoint::from_shared(server_url.to_string())? + .http2_keep_alive_interval(Duration::new(5, 0)) + .tcp_keepalive(Some(Duration::from_secs(5))) + .keep_alive_timeout(Duration::from_secs(5)) + .keep_alive_while_idle(true) + .tls_config(ClientTlsConfig::new().with_enabled_roots())?) + } + + fn api_key_metadata(&self) -> Result>, Error> { + match &self.api_key { + Some(key) => Ok(Some(format!("Bearer {key}").parse().map_err( + |e: InvalidMetadataValue| { + anyhow!(format!( + "(Breez: {:?}) Failed parse API key: {e}", + self.api_key + )) + }, + )?)), + _ => Ok(None), + } + } +} + +impl BreezSyncerClient { + async fn get_client( + &self, + ) -> Result>, Error> { + let Some(channel) = self.grpc_channel.lock().await.clone() else { + return Err(anyhow!("Cannot get sync client: not connected")); + }; + let api_key_metadata = self.api_key_metadata()?; + Ok(ProtoSyncerClient::with_interceptor( + channel, + ApiKeyInterceptor { api_key_metadata }, + )) + } +} + +#[async_trait] +impl SyncerClient for BreezSyncerClient { + async fn connect(&self, connect_url: String) -> Result<()> { + let mut grpc_channel = self.grpc_channel.lock().await; + *grpc_channel = Some(Self::create_endpoint(&connect_url)?.connect_lazy()); + debug!("Successfully connected to {connect_url}"); + Ok(()) + } + + async fn push(&self, req: SetRecordRequest) -> Result { + Ok(self.get_client().await?.set_record(req).await?.into_inner()) + } + + async fn pull(&self, req: ListChangesRequest) -> Result { + Ok(self + .get_client() + .await? + .list_changes(req) + .await? + .into_inner()) + } + + async fn disconnect(&self) -> Result<()> { + let mut channel = self.grpc_channel.lock().await; + *channel = None; + Ok(()) + } +} + +#[derive(Clone)] +pub struct ApiKeyInterceptor { + api_key_metadata: Option>, +} + +impl Interceptor for ApiKeyInterceptor { + fn call(&mut self, mut req: Request<()>) -> Result, Status> { + if self.api_key_metadata.clone().is_some() { + req.metadata_mut() + .insert("authorization", self.api_key_metadata.clone().unwrap()); + } + Ok(req) + } +} diff --git a/lib/core/src/sync/mod.rs b/lib/core/src/sync/mod.rs new file mode 100644 index 000000000..e32511a4d --- /dev/null +++ b/lib/core/src/sync/mod.rs @@ -0,0 +1,815 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, Result}; +use futures_util::TryFutureExt; +use model::data::PaymentDetailsSyncData; +use tokio::sync::mpsc::Receiver; +use tokio::sync::{watch, Mutex}; + +use crate::prelude::Swap; +use crate::recover::recoverer::Recoverer; +use crate::sync::model::sync::{Record, SetRecordRequest, SetRecordStatus}; +use crate::sync::model::DecryptionInfo; +use crate::utils; +use crate::{ + persist::{cache::KEY_LAST_DERIVATION_INDEX, Persister}, + prelude::Signer, +}; + +use self::client::SyncerClient; +use self::model::{ + data::{ChainSyncData, ReceiveSyncData, SendSyncData, SyncData}, + sync::ListChangesRequest, + RecordType, SyncState, +}; +use self::model::{DecryptionError, SyncOutgoingChanges}; + +pub(crate) mod client; +pub(crate) mod model; + +pub(crate) struct SyncService { + remote_url: String, + persister: Arc, + recoverer: Arc, + signer: Arc>, + client: Box, + sync_trigger: Mutex>, +} + +impl SyncService { + pub(crate) fn new( + remote_url: String, + persister: Arc, + recoverer: Arc, + signer: Arc>, + client: Box, + sync_trigger: Receiver<()>, + ) -> Self { + let sync_trigger = Mutex::new(sync_trigger); + Self { + remote_url, + persister, + recoverer, + signer, + client, + sync_trigger, + } + } + + fn check_remote_change(&self) -> Result<()> { + match self + .persister + .get_sync_settings()? + .remote_url + .is_some_and(|url| url == self.remote_url) + { + true => Ok(()), + false => self.persister.set_new_remote(self.remote_url.clone()), + } + } + + async fn run_event_loop(&self) { + if let Err(err) = self.pull().and_then(|_| self.push()).await { + log::debug!("Could not run sync event loop: {err:?}"); + } + } + + pub(crate) async fn start(self: Arc, mut shutdown: watch::Receiver<()>) -> Result<()> { + tokio::spawn(async move { + if let Err(err) = self.client.connect(self.remote_url.clone()).await { + log::warn!("Could not connect to sync service: {err:?}"); + return; + } + if let Err(err) = self.check_remote_change() { + log::warn!("Could not check for remote change: {err:?}"); + return; + } + + let mut sync_trigger = self.sync_trigger.lock().await; + let mut event_loop_interval = tokio::time::interval(Duration::from_secs(30)); + + loop { + tokio::select! { + _ = event_loop_interval.tick() => self.run_event_loop().await, + Some(_) = sync_trigger.recv() => { + self.run_event_loop().await; + event_loop_interval.reset(); + } + _ = shutdown.changed() => { + log::info!("Received shutdown signal, exiting realtime sync service loop"); + if let Err(err) = self.client.disconnect().await { + log::debug!("Could not disconnect sync service client: {err:?}"); + }; + return; + } + } + } + }); + + Ok(()) + } + + fn commit_record(&self, decryption_info: &DecryptionInfo, swap: Option) -> Result<()> { + let DecryptionInfo { + record, + new_sync_state, + last_commit_time, + .. + } = decryption_info; + match record.data.clone() { + SyncData::Chain(_) | SyncData::Receive(_) | SyncData::Send(_) => { + let Some(swap) = swap else { + return Err(anyhow!( + "Cannot commit a swap-related record without specifying a swap." + )); + }; + self.persister + .commit_incoming_swap(&swap, new_sync_state, *last_commit_time) + } + SyncData::LastDerivationIndex(new_address_index) => { + self.persister.commit_incoming_address_index( + new_address_index, + new_sync_state, + *last_commit_time, + ) + } + SyncData::PaymentDetails(payment_details_sync_data) => { + self.persister.commit_incoming_payment_details( + payment_details_sync_data.into(), + new_sync_state, + *last_commit_time, + ) + } + } + } + + fn load_sync_data(&self, data_id: &str, record_type: RecordType) -> Result { + let data = match record_type { + RecordType::Receive => { + let receive_data: ReceiveSyncData = self + .persister + .fetch_receive_swap_by_id(data_id)? + .ok_or(anyhow!("Could not find Receive swap {data_id}"))? + .into(); + SyncData::Receive(receive_data) + } + RecordType::Send => { + let send_data: SendSyncData = self + .persister + .fetch_send_swap_by_id(data_id)? + .ok_or(anyhow!("Could not find Send swap {data_id}"))? + .into(); + SyncData::Send(send_data) + } + RecordType::Chain => { + let chain_data: ChainSyncData = self + .persister + .fetch_chain_swap_by_id(data_id)? + .ok_or(anyhow!("Could not find Chain swap {data_id}"))? + .into(); + SyncData::Chain(chain_data) + } + RecordType::LastDerivationIndex => SyncData::LastDerivationIndex( + self.persister + .get_cached_item(KEY_LAST_DERIVATION_INDEX)? + .ok_or(anyhow!("Could not find last derivation index"))? + .parse()?, + ), + RecordType::PaymentDetails => { + let payment_details_data: PaymentDetailsSyncData = self + .persister + .get_payment_details(data_id)? + .ok_or(anyhow!("Could not find Payment Details {data_id}"))? + .into(); + SyncData::PaymentDetails(payment_details_data) + } + }; + Ok(data) + } + + async fn fetch_and_save_records(&self) -> Result<()> { + log::info!("Initiating record pull"); + + let local_latest_revision = self + .persister + .get_sync_settings()? + .latest_revision + .unwrap_or(0); + let req = ListChangesRequest::new(local_latest_revision, self.signer.clone())?; + let incoming_records = self.client.pull(req).await?.changes; + + self.persister.set_incoming_records(&incoming_records)?; + let remote_latest_revision = incoming_records.last().map(|record| record.revision); + if let Some(latest_revision) = remote_latest_revision { + self.persister.set_sync_settings(HashMap::from([( + "latest_revision", + latest_revision.to_string(), + )]))?; + log::info!( + "Successfully pulled and persisted records. New latest revision: {latest_revision}" + ); + } else { + log::info!("No new records found. Local latest revision: {local_latest_revision}"); + } + + Ok(()) + } + + async fn handle_decryption( + &self, + new_record: Record, + ) -> Result { + log::debug!( + "Handling decryption for record record_id {}", + &new_record.id + ); + + // Step 3: Check whether or not record is applicable (from its schema_version) + if !new_record.is_applicable()? { + return Err(DecryptionError::SchemaNotApplicable); + } + + // Step 4: Check whether we already have this record, and if the revision is newer + let maybe_sync_state = self.persister.get_sync_state_by_record_id(&new_record.id)?; + if let Some(sync_state) = &maybe_sync_state { + if sync_state.record_revision >= new_record.revision { + return Err(DecryptionError::AlreadyPersisted); + } + } + + // Step 5: Decrypt the incoming record + let mut decrypted_record = new_record.decrypt(self.signer.clone())?; + + // Step 6: Merge with outgoing records, if present + let maybe_outgoing_changes = self + .persister + .get_sync_outgoing_changes_by_id(&decrypted_record.id)?; + + if let Some(outgoing_changes) = &maybe_outgoing_changes { + if let Some(updated_fields) = &outgoing_changes.updated_fields { + let local_data = + self.load_sync_data(decrypted_record.data.id(), outgoing_changes.record_type)?; + decrypted_record.data.merge(&local_data, updated_fields)?; + } + } + + let new_sync_state = SyncState { + data_id: decrypted_record.data.id().to_string(), + record_id: decrypted_record.id.clone(), + record_revision: decrypted_record.revision, + is_local: maybe_sync_state + .as_ref() + .map(|state| state.is_local) + .unwrap_or(false), + }; + let last_commit_time = maybe_outgoing_changes.map(|details| details.commit_time); + + log::debug!("Successfully decrypted record {}", &decrypted_record.id); + + Ok(DecryptionInfo { + new_sync_state, + record: decrypted_record, + last_commit_time, + }) + } + + async fn handle_recovery( + &self, + swap_decryption_info: Vec, + ) -> Result> { + let mut succeded = vec![]; + let mut swaps = vec![]; + + // Step 1: Convert each record into a swap, if possible + for decryption_info in swap_decryption_info { + let record = &decryption_info.record; + match TryInto::::try_into(record.data.clone()) { + Ok(swap) => { + succeded.push(decryption_info); + swaps.push(swap); + } + Err(e) => { + log::warn!("Could not convert sync data to swap: {e}"); + continue; + } + }; + } + + // Step 2: Recover each swap's data from chain + self.recoverer.recover_from_onchain(&mut swaps).await?; + + Ok(succeded.into_iter().zip(swaps.into_iter()).collect()) + } + + pub(crate) async fn pull(&self) -> Result<()> { + // Step 1: Fetch and save incoming records from remote, then update local tip + self.fetch_and_save_records().await?; + + // Step 2: Grab all pending incoming records from the database + let incoming_records = self.persister.get_incoming_records()?; + + // Step 3: Decrypt all the records, if possible. Filter those whose revision/schema is not + // applicable + let mut succeded = vec![]; + let mut decrypted: Vec = vec![]; + for record in incoming_records { + let record_id = record.id.clone(); + match self.handle_decryption(record).await { + Ok(decryption_info) => decrypted.push(decryption_info), + // If we already have this record, it should be cleaned from sync_incoming + Err(DecryptionError::AlreadyPersisted) => succeded.push(record_id), + Err(e) => { + log::debug!( + "Could not handle decryption of incoming record {record_id}: {e:?}", + ); + continue; + } + } + } + + // Step 4: Split each record into two categories: swap and non-swap + let (decrypted_swap_info, decrypted_non_swap_info): ( + Vec, + Vec, + ) = decrypted + .into_iter() + .partition(|result| result.record.data.is_swap()); + + // Step 5: Recover the swap records' data from onchain, and commit it + for (decryption_info, swap) in self.handle_recovery(decrypted_swap_info).await? { + if let Err(e) = self.commit_record(&decryption_info, Some(swap)) { + log::warn!("Could not commit swap record: {e:?}"); + continue; + } + succeded.push(decryption_info.record.id); + } + + // Step 6: Commit non-swap-related data + for decryption_info in decrypted_non_swap_info { + if let Err(e) = self.commit_record(&decryption_info, None) { + log::warn!("Could not commit generic record: {e:?}"); + continue; + } + succeded.push(decryption_info.record.id); + } + + // Step 7: Clear succeded records + if !succeded.is_empty() { + self.persister.remove_incoming_records(succeded)?; + } + + Ok(()) + } + + async fn handle_push( + &self, + record_id: &str, + data_id: &str, + record_type: RecordType, + ) -> Result<()> { + log::debug!("Handling push for record record_id {record_id} data_id {data_id}"); + + // Step 1: Get the sync state, if it exists, to compute the revision + let maybe_sync_state = self.persister.get_sync_state_by_record_id(record_id)?; + let record_revision = maybe_sync_state + .as_ref() + .map(|s| s.record_revision) + .unwrap_or(0); + let is_local = maybe_sync_state.map(|s| s.is_local).unwrap_or(true); + + // Step 2: Fetch the sync data + let sync_data = self.load_sync_data(data_id, record_type)?; + + // Step 3: Create the record to push outwards + let record = Record::new(sync_data, record_revision, self.signer.clone())?; + + // Step 4: Push the record + let req = SetRecordRequest::new(record, utils::now(), self.signer.clone())?; + let reply = self.client.push(req).await?; + + // Step 5: Check for conflict. If present, skip and retry on the next call + if reply.status() == SetRecordStatus::Conflict { + return Err(anyhow!( + "Got conflict status when attempting to push record" + )); + } + + // Step 6: Set/update the state revision + self.persister.set_sync_state(SyncState { + data_id: data_id.to_string(), + record_id: record_id.to_string(), + record_revision: reply.new_revision, + is_local, + })?; + + log::info!("Successfully pushed record record_id {record_id}"); + + Ok(()) + } + + async fn push(&self) -> Result<()> { + let outgoing_changes = self.persister.get_sync_outgoing_changes()?; + + let mut succeded = vec![]; + for SyncOutgoingChanges { + record_id, + data_id, + record_type, + .. + } in outgoing_changes + { + if let Err(err) = self.handle_push(&record_id, &data_id, record_type).await { + log::debug!("Could not handle push for record {record_id}: {err:?}"); + continue; + } + succeded.push(record_id); + } + + if !succeded.is_empty() { + self.persister.remove_sync_outgoing_changes(succeded)?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use anyhow::{anyhow, Result}; + use std::{collections::HashMap, sync::Arc}; + + use crate::{ + persist::{cache::KEY_LAST_DERIVATION_INDEX, Persister}, + prelude::{Direction, PaymentState, Signer}, + sync::model::{data::LAST_DERIVATION_INDEX_DATA_ID, SyncState}, + test_utils::{ + chain_swap::new_chain_swap, + persist::{create_persister, new_receive_swap, new_send_swap}, + recover::new_recoverer, + sync::{ + new_chain_sync_data, new_receive_sync_data, new_send_sync_data, new_sync_service, + }, + wallet::{MockSigner, MockWallet}, + }, + }; + + use super::model::{data::SyncData, sync::Record, RecordType}; + + #[tokio::test] + async fn test_incoming_sync_create_and_update() -> Result<()> { + create_persister!(persister); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?); + let recoverer = Arc::new(new_recoverer(signer.clone(), onchain_wallet.clone())?); + + let sync_data = vec![ + SyncData::Receive(new_receive_sync_data()), + SyncData::Send(new_send_sync_data(None)), + SyncData::Chain(new_chain_sync_data(None)), + ]; + let incoming_records = vec![ + Record::new(sync_data[0].clone(), 1, signer.clone())?, + Record::new(sync_data[1].clone(), 2, signer.clone())?, + Record::new(sync_data[2].clone(), 3, signer.clone())?, + ]; + + let (incoming_tx, _outgoing_records, sync_service) = + new_sync_service(persister.clone(), recoverer, signer.clone())?; + + for record in incoming_records { + incoming_tx.send(record).await?; + } + sync_service.pull().await?; + + if let Some(receive_swap) = persister.fetch_receive_swap_by_id(sync_data[0].id())? { + assert!(receive_swap.description.is_none()); + assert!(receive_swap.payment_hash.is_none()); + } else { + return Err(anyhow!("Receive swap not found")); + } + if let Some(send_swap) = persister.fetch_send_swap_by_id(sync_data[1].id())? { + assert!(send_swap.preimage.is_none()); + assert!(send_swap.description.is_none()); + assert!(send_swap.payment_hash.is_none()); + } else { + return Err(anyhow!("Send swap not found")); + } + if let Some(chain_swap) = persister.fetch_chain_swap_by_id(sync_data[2].id())? { + assert!(chain_swap.claim_address.is_none()); + assert!(chain_swap.description.is_none()); + assert!(chain_swap.accept_zero_conf.eq(&true)); + } else { + return Err(anyhow!("Chain swap not found")); + } + + let new_preimage = Some("preimage".to_string()); + let new_accept_zero_conf = false; + let sync_data = vec![ + SyncData::Send(new_send_sync_data(new_preimage.clone())), + SyncData::Chain(new_chain_sync_data(Some(new_accept_zero_conf))), + ]; + let incoming_records = vec![ + Record::new(sync_data[0].clone(), 4, signer.clone())?, + Record::new(sync_data[1].clone(), 5, signer.clone())?, + ]; + + for record in incoming_records { + incoming_tx.send(record).await?; + } + sync_service.pull().await?; + + if let Some(send_swap) = persister.fetch_send_swap_by_id(sync_data[0].id())? { + assert_eq!(send_swap.preimage, new_preimage); + } else { + return Err(anyhow!("Send swap not found")); + } + if let Some(chain_swap) = persister.fetch_chain_swap_by_id(sync_data[1].id())? { + assert_eq!(chain_swap.accept_zero_conf, new_accept_zero_conf); + } else { + return Err(anyhow!("Chain swap not found")); + } + + Ok(()) + } + + fn get_outgoing_record<'a>( + persister: Arc, + outgoing: &'a HashMap, + data_id: &str, + record_type: RecordType, + ) -> Result<&'a Record> { + let record_id = Record::get_id_from_record_type(record_type, data_id); + let sync_state = persister + .get_sync_state_by_record_id(&record_id)? + .ok_or(anyhow::anyhow!("Expected existing swap state"))?; + let Some(record) = outgoing.get(&sync_state.record_id) else { + return Err(anyhow::anyhow!( + "Expecting existing record in client's outgoing list" + )); + }; + Ok(record) + } + + #[tokio::test] + async fn test_outgoing_sync() -> Result<()> { + create_persister!(persister); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?); + let recoverer = Arc::new(new_recoverer(signer.clone(), onchain_wallet.clone())?); + + let (_incoming_tx, outgoing_records, sync_service) = + new_sync_service(persister.clone(), recoverer, signer.clone())?; + + // Test insert + persister.insert_or_update_receive_swap(&new_receive_swap(None))?; + persister.insert_or_update_send_swap(&new_send_swap(None))?; + persister.insert_or_update_chain_swap(&new_chain_swap( + Direction::Incoming, + None, + true, + None, + ))?; + + sync_service.push().await?; + + let outgoing = outgoing_records.lock().await; + assert_eq!(outgoing.len(), 3); + drop(outgoing); + + // Test conflict + let swap = new_receive_swap(None); + persister.insert_or_update_receive_swap(&swap)?; + + sync_service.push().await?; + + let outgoing = outgoing_records.lock().await; + assert_eq!(outgoing.len(), 4); + let record = + get_outgoing_record(persister.clone(), &outgoing, &swap.id, RecordType::Receive)?; + persister.set_sync_state(SyncState { + data_id: swap.id.clone(), + record_id: record.id.clone(), + record_revision: 90, // Set a wrong record revision + is_local: true, + })?; + drop(outgoing); + + sync_service.push().await?; + + let outgoing = outgoing_records.lock().await; + assert_eq!(outgoing.len(), 4); // No records were added + drop(outgoing); + + // Test update before push + let swap = new_send_swap(None); + persister.insert_or_update_send_swap(&swap)?; + let new_preimage = Some("new-preimage"); + persister.try_handle_send_swap_update( + &swap.id, + PaymentState::Pending, + new_preimage, + None, + None, + )?; + + sync_service.push().await?; + + let outgoing = outgoing_records.lock().await; + + let record = get_outgoing_record(persister.clone(), &outgoing, &swap.id, RecordType::Send)?; + let decrypted_record = record.clone().decrypt(signer.clone())?; + assert_eq!(decrypted_record.data.id(), &swap.id); + match decrypted_record.data { + SyncData::Send(data) => { + assert_eq!(data.preimage, new_preimage.map(|p| p.to_string())); + } + _ => { + return Err(anyhow::anyhow!("Unexpected sync data type received.")); + } + } + drop(outgoing); + + // Test update after push + let swap = new_send_swap(None); + persister.insert_or_update_send_swap(&swap)?; + + sync_service.push().await?; + + let new_preimage = Some("new-preimage"); + persister.try_handle_send_swap_update( + &swap.id, + PaymentState::Pending, + new_preimage, + None, + None, + )?; + + sync_service.push().await?; + + let outgoing = outgoing_records.lock().await; + let record = get_outgoing_record(persister.clone(), &outgoing, &swap.id, RecordType::Send)?; + let decrypted_record = record.clone().decrypt(signer.clone())?; + assert_eq!(decrypted_record.data.id(), &swap.id); + match decrypted_record.data { + SyncData::Send(data) => { + assert_eq!(data.preimage, new_preimage.map(|p| p.to_string()),); + } + _ => { + return Err(anyhow::anyhow!("Unexpected sync data type received.")); + } + } + + Ok(()) + } + + #[tokio::test] + async fn test_sync_clean() -> Result<()> { + create_persister!(persister); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?); + let recoverer = Arc::new(new_recoverer(signer.clone(), onchain_wallet.clone())?); + + let (incoming_tx, _outgoing_records, sync_service) = + new_sync_service(persister.clone(), recoverer, signer.clone())?; + + // Clean incoming + let record = Record::new( + SyncData::Receive(new_receive_sync_data()), + 1, + signer.clone(), + )?; + incoming_tx.send(record).await?; + sync_service.pull().await?; + + let incoming_records = persister.get_incoming_records()?; + assert_eq!(incoming_records.len(), 0); // Records have been cleaned + + let mut inapplicable_record = Record::new( + SyncData::Receive(new_receive_sync_data()), + 2, + signer.clone(), + )?; + inapplicable_record.schema_version = "9.9.9".to_string(); + incoming_tx.send(inapplicable_record).await?; + sync_service.pull().await?; + + let incoming_records = persister.get_incoming_records()?; + assert_eq!(incoming_records.len(), 1); // Inapplicable records are stored for later + + // Clean outgoing + let swap = new_send_swap(None); + persister.insert_or_update_send_swap(&swap)?; + let outgoing_changes = persister.get_sync_outgoing_changes()?; + assert_eq!(outgoing_changes.len(), 1); // Changes have been set + + sync_service.push().await?; + let outgoing_changes = persister.get_sync_outgoing_changes()?; + assert_eq!(outgoing_changes.len(), 0); // Changes have been cleaned + + let new_preimage = Some("new-preimage"); + persister.try_handle_send_swap_update( + &swap.id, + PaymentState::Pending, + new_preimage, + None, + None, + )?; + let outgoing_changes = persister.get_sync_outgoing_changes()?; + assert_eq!(outgoing_changes.len(), 1); // Changes have been set + + sync_service.push().await?; + let outgoing_changes = persister.get_sync_outgoing_changes()?; + assert_eq!(outgoing_changes.len(), 0); // Changes have been cleaned + + Ok(()) + } + + #[tokio::test] + async fn test_last_derivation_index_update() -> Result<()> { + create_persister!(persister); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?); + let recoverer = Arc::new(new_recoverer(signer.clone(), onchain_wallet.clone())?); + + let (incoming_tx, outgoing_records, sync_service) = + new_sync_service(persister.clone(), recoverer, signer.clone())?; + + // Check pull + assert_eq!(persister.get_cached_item(KEY_LAST_DERIVATION_INDEX)?, None); + + let new_last_derivation_index = 10; + let data = SyncData::LastDerivationIndex(new_last_derivation_index); + incoming_tx + .send(Record::new(data, 0, signer.clone())?) + .await?; + + sync_service.pull().await?; + + assert_eq!( + persister.get_cached_item(KEY_LAST_DERIVATION_INDEX)?, + Some(new_last_derivation_index.to_string()) + ); + + // Check push + let new_last_derivation_index = 20; + persister.set_last_derivation_index(new_last_derivation_index)?; + + sync_service.push().await?; + + let outgoing = outgoing_records.lock().await; + let record = get_outgoing_record( + persister.clone(), + &outgoing, + LAST_DERIVATION_INDEX_DATA_ID, + RecordType::LastDerivationIndex, + )?; + let decrypted_record = record.clone().decrypt(signer.clone())?; + match decrypted_record.data { + SyncData::LastDerivationIndex(last_derivation_index) => { + assert_eq!(last_derivation_index, new_last_derivation_index); + } + _ => { + return Err(anyhow::anyhow!("Unexpected sync data type received.")); + } + } + + // Check pull with merge + let new_local_last_derivation_index = 30; + persister.set_last_derivation_index(new_local_last_derivation_index)?; + + let new_remote_last_derivation_index = 25; + let data = SyncData::LastDerivationIndex(new_remote_last_derivation_index); + incoming_tx + .send(Record::new(data, 0, signer.clone())?) + .await?; + + sync_service.pull().await?; + + // Newer one is persisted (local > remote) + assert_eq!( + persister.get_cached_item(KEY_LAST_DERIVATION_INDEX)?, + Some(new_local_last_derivation_index.to_string()) + ); + + let new_local_last_derivation_index = 35; + persister.set_last_derivation_index(new_local_last_derivation_index)?; + + let new_remote_last_derivation_index = 40; + let data = SyncData::LastDerivationIndex(new_remote_last_derivation_index); + incoming_tx + .send(Record::new(data, 2, signer.clone())?) + .await?; + + sync_service.pull().await?; + + // Newer one is persisted (remote > local) + assert_eq!( + persister.get_cached_item(KEY_LAST_DERIVATION_INDEX)?, + Some(new_remote_last_derivation_index.to_string()) + ); + + Ok(()) + } +} diff --git a/lib/core/src/sync/model/client.rs b/lib/core/src/sync/model/client.rs new file mode 100644 index 000000000..897ff8109 --- /dev/null +++ b/lib/core/src/sync/model/client.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use lwk_wollet::hashes::hex::DisplayHex as _; +use openssl::sha::sha256; +use std::sync::Arc; + +use crate::{ + prelude::{Signer, SignerError}, + utils, +}; + +use super::{ + sync::{ListChangesRequest, Record, SetRecordRequest}, + CURRENT_SCHEMA_VERSION, MESSAGE_PREFIX, +}; + +fn sign_message(msg: &[u8], signer: Arc>) -> Result { + let msg = [MESSAGE_PREFIX, msg].concat(); + let digest = sha256(&sha256(&msg)); + signer + .sign_ecdsa_recoverable(digest.into()) + .map(|bytes| zbase32::encode_full_bytes(&bytes)) +} + +impl ListChangesRequest { + pub(crate) fn new(since_revision: u64, signer: Arc>) -> Result { + let request_time = utils::now(); + let msg = format!("{}-{}", since_revision, request_time); + let signature = sign_message(msg.as_bytes(), signer)?; + Ok(Self { + since_revision, + request_time, + signature, + }) + } +} +impl SetRecordRequest { + pub(crate) fn new( + record: Record, + request_time: u32, + signer: Arc>, + ) -> Result { + let msg = format!( + "{}-{}-{}-{}-{}", + record.id, + record.data.to_lower_hex_string(), + record.revision, + *CURRENT_SCHEMA_VERSION, + request_time, + ); + let signature = sign_message(msg.as_bytes(), signer)?; + Ok(Self { + record: Some(record), + request_time, + signature, + }) + } +} diff --git a/lib/core/src/sync/model/data.rs b/lib/core/src/sync/model/data.rs new file mode 100644 index 000000000..cdd681884 --- /dev/null +++ b/lib/core/src/sync/model/data.rs @@ -0,0 +1,341 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + persist::model::PaymentTxDetails, + prelude::{ChainSwap, Direction, LnUrlInfo, PaymentState, ReceiveSwap, SendSwap, Swap}, +}; + +pub(crate) const LAST_DERIVATION_INDEX_DATA_ID: &str = "last-derivation-index"; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct ChainSyncData { + pub(crate) swap_id: String, + pub(crate) preimage: String, + pub(crate) pair_fees_json: String, + pub(crate) create_response_json: String, + pub(crate) direction: Direction, + pub(crate) lockup_address: String, + pub(crate) claim_fees_sat: u64, + pub(crate) claim_private_key: String, + pub(crate) refund_private_key: String, + pub(crate) timeout_block_height: u32, + pub(crate) payer_amount_sat: u64, + pub(crate) receiver_amount_sat: u64, + pub(crate) accept_zero_conf: bool, + pub(crate) created_at: u32, + pub(crate) description: Option, +} + +impl ChainSyncData { + pub(crate) fn merge(&mut self, other: &Self, updated_fields: &[String]) { + for field in updated_fields { + match field.as_str() { + "payer_amount_sat" => self.payer_amount_sat = other.payer_amount_sat, + "receiver_amount_sat" => self.receiver_amount_sat = other.receiver_amount_sat, + "accept_zero_conf" => self.accept_zero_conf = other.accept_zero_conf, + _ => continue, + } + } + } +} + +impl From for ChainSyncData { + fn from(value: ChainSwap) -> Self { + Self { + swap_id: value.id, + preimage: value.preimage, + pair_fees_json: value.pair_fees_json, + create_response_json: value.create_response_json, + direction: value.direction, + lockup_address: value.lockup_address, + claim_fees_sat: value.claim_fees_sat, + claim_private_key: value.claim_private_key, + refund_private_key: value.refund_private_key, + timeout_block_height: value.timeout_block_height, + payer_amount_sat: value.payer_amount_sat, + receiver_amount_sat: value.receiver_amount_sat, + accept_zero_conf: value.accept_zero_conf, + created_at: value.created_at, + description: value.description, + } + } +} + +impl From for ChainSwap { + fn from(val: ChainSyncData) -> Self { + ChainSwap { + id: val.swap_id, + direction: val.direction, + lockup_address: val.lockup_address, + timeout_block_height: val.timeout_block_height, + preimage: val.preimage, + description: val.description, + payer_amount_sat: val.payer_amount_sat, + receiver_amount_sat: val.receiver_amount_sat, + claim_fees_sat: val.claim_fees_sat, + accept_zero_conf: val.accept_zero_conf, + pair_fees_json: val.pair_fees_json, + create_response_json: val.create_response_json, + created_at: val.created_at, + claim_private_key: val.claim_private_key, + refund_private_key: val.refund_private_key, + state: PaymentState::Created, + claim_address: None, + server_lockup_tx_id: None, + user_lockup_tx_id: None, + claim_tx_id: None, + refund_tx_id: None, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct SendSyncData { + pub(crate) swap_id: String, + pub(crate) invoice: String, + pub(crate) pair_fees_json: String, + pub(crate) create_response_json: String, + pub(crate) refund_private_key: String, + pub(crate) payer_amount_sat: u64, + pub(crate) receiver_amount_sat: u64, + pub(crate) created_at: u32, + pub(crate) preimage: Option, + pub(crate) bolt12_offer: Option, + pub(crate) payment_hash: Option, + pub(crate) description: Option, +} + +impl SendSyncData { + pub(crate) fn merge(&mut self, other: &Self, updated_fields: &[String]) { + for field in updated_fields { + match field.as_str() { + "preimage" => clone_if_set(&mut self.preimage, &other.preimage), + _ => continue, + } + } + } +} + +impl From for SendSyncData { + fn from(value: SendSwap) -> Self { + Self { + swap_id: value.id, + payment_hash: value.payment_hash, + invoice: value.invoice, + pair_fees_json: value.pair_fees_json, + create_response_json: value.create_response_json, + refund_private_key: value.refund_private_key, + payer_amount_sat: value.payer_amount_sat, + receiver_amount_sat: value.receiver_amount_sat, + created_at: value.created_at, + preimage: value.preimage, + description: value.description, + bolt12_offer: value.bolt12_offer, + } + } +} + +impl From for SendSwap { + fn from(val: SendSyncData) -> Self { + SendSwap { + id: val.swap_id, + invoice: val.invoice, + payment_hash: val.payment_hash, + description: val.description, + preimage: val.preimage, + payer_amount_sat: val.payer_amount_sat, + receiver_amount_sat: val.receiver_amount_sat, + pair_fees_json: val.pair_fees_json, + create_response_json: val.create_response_json, + created_at: val.created_at, + refund_private_key: val.refund_private_key, + bolt12_offer: val.bolt12_offer, + state: PaymentState::Created, + lockup_tx_id: None, + refund_tx_id: None, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct ReceiveSyncData { + pub(crate) swap_id: String, + pub(crate) invoice: String, + pub(crate) preimage: String, + pub(crate) pair_fees_json: String, + pub(crate) create_response_json: String, + pub(crate) claim_fees_sat: u64, + pub(crate) claim_private_key: String, + pub(crate) payer_amount_sat: u64, + pub(crate) receiver_amount_sat: u64, + pub(crate) mrh_address: String, + pub(crate) created_at: u32, + pub(crate) payment_hash: Option, + pub(crate) description: Option, +} + +impl From for ReceiveSyncData { + fn from(value: ReceiveSwap) -> Self { + Self { + swap_id: value.id, + payment_hash: value.payment_hash, + invoice: value.invoice, + preimage: value.preimage, + pair_fees_json: value.pair_fees_json, + create_response_json: value.create_response_json, + claim_fees_sat: value.claim_fees_sat, + claim_private_key: value.claim_private_key, + payer_amount_sat: value.payer_amount_sat, + receiver_amount_sat: value.receiver_amount_sat, + mrh_address: value.mrh_address, + created_at: value.created_at, + description: value.description, + } + } +} + +impl From for ReceiveSwap { + fn from(val: ReceiveSyncData) -> Self { + ReceiveSwap { + id: val.swap_id, + preimage: val.preimage, + create_response_json: val.create_response_json, + pair_fees_json: val.pair_fees_json, + claim_private_key: val.claim_private_key, + invoice: val.invoice, + payment_hash: val.payment_hash, + description: val.description, + payer_amount_sat: val.payer_amount_sat, + receiver_amount_sat: val.receiver_amount_sat, + claim_fees_sat: val.claim_fees_sat, + mrh_address: val.mrh_address, + created_at: val.created_at, + state: PaymentState::Created, + claim_tx_id: None, + lockup_tx_id: None, + mrh_tx_id: None, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct PaymentDetailsSyncData { + pub(crate) tx_id: String, + pub(crate) destination: String, + pub(crate) description: Option, + pub(crate) lnurl_info: Option, +} + +impl PaymentDetailsSyncData { + pub(crate) fn merge(&mut self, other: &Self, updated_fields: &[String]) { + for field in updated_fields { + match field.as_str() { + "destination" => self.destination = other.destination.clone(), + "description" => clone_if_set(&mut self.description, &other.description), + "lnurl_info" => clone_if_set(&mut self.lnurl_info, &other.lnurl_info), + _ => continue, + } + } + } +} + +impl From for PaymentDetailsSyncData { + fn from(value: PaymentTxDetails) -> Self { + Self { + tx_id: value.tx_id, + destination: value.destination, + description: value.description, + lnurl_info: value.lnurl_info, + } + } +} + +impl From for PaymentTxDetails { + fn from(val: PaymentDetailsSyncData) -> Self { + PaymentTxDetails { + tx_id: val.tx_id, + destination: val.destination, + description: val.description, + lnurl_info: val.lnurl_info, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "data_type", content = "data")] +pub(crate) enum SyncData { + Chain(ChainSyncData), + Send(SendSyncData), + Receive(ReceiveSyncData), + LastDerivationIndex(u32), + PaymentDetails(PaymentDetailsSyncData), +} + +impl SyncData { + pub(crate) fn id(&self) -> &str { + match self { + SyncData::Chain(chain_data) => &chain_data.swap_id, + SyncData::Send(send_data) => &send_data.swap_id, + SyncData::Receive(receive_data) => &receive_data.swap_id, + SyncData::LastDerivationIndex(_) => LAST_DERIVATION_INDEX_DATA_ID, + SyncData::PaymentDetails(payment_details) => &payment_details.tx_id, + } + } + + pub(crate) fn to_bytes(&self) -> serde_json::Result> { + serde_json::to_vec(self) + } + + /// Whether the data is a swap + pub(crate) fn is_swap(&self) -> bool { + match self { + SyncData::LastDerivationIndex(_) | SyncData::PaymentDetails(_) => false, + SyncData::Chain(_) | SyncData::Send(_) | SyncData::Receive(_) => true, + } + } + + pub(crate) fn merge(&mut self, other: &Self, updated_fields: &[String]) -> anyhow::Result<()> { + match (self, other) { + (SyncData::Chain(ref mut base), SyncData::Chain(other)) => { + base.merge(other, updated_fields) + } + (SyncData::Send(ref mut base), SyncData::Send(other)) => { + base.merge(other, updated_fields) + } + (SyncData::Receive(ref mut _base), SyncData::Receive(_other)) => { + log::warn!("Attempting to merge for unnecessary type SyncData::Receive"); + } + ( + SyncData::LastDerivationIndex(our_index), + SyncData::LastDerivationIndex(their_index), + ) => { + *our_index = std::cmp::max(*their_index, *our_index); + } + (SyncData::PaymentDetails(ref mut base), SyncData::PaymentDetails(other)) => { + base.merge(other, updated_fields) + } + _ => return Err(anyhow::anyhow!("Cannot merge data from two separate types")), + }; + Ok(()) + } +} + +impl TryInto for SyncData { + type Error = anyhow::Error; + fn try_into(self) -> std::result::Result { + match self { + SyncData::Chain(chain_data) => Ok(Swap::Chain(chain_data.into())), + SyncData::Send(send_data) => Ok(Swap::Send(send_data.into())), + SyncData::Receive(receive_data) => Ok(Swap::Receive(receive_data.into())), + _ => Err(anyhow::anyhow!( + "Cannot convert this sync data type to a swap" + )), + } + } +} + +fn clone_if_set(s: &mut Option, other: &Option) { + if other.is_some() { + s.clone_from(other) + } +} diff --git a/lib/core/src/sync/model/mod.rs b/lib/core/src/sync/model/mod.rs new file mode 100644 index 000000000..7038cc4e5 --- /dev/null +++ b/lib/core/src/sync/model/mod.rs @@ -0,0 +1,197 @@ +use std::sync::Arc; + +use self::{data::SyncData, sync::Record}; +use crate::prelude::Signer; +use anyhow::Result; +use lazy_static::lazy_static; +use lwk_wollet::hashes::hex::DisplayHex; +use openssl::sha::sha256; +use rusqlite::{ + types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}, + ToSql, +}; +use semver::Version; + +pub(crate) mod client; +pub(crate) mod data; +pub(crate) mod sync; + +const MESSAGE_PREFIX: &[u8; 13] = b"realtimesync:"; +lazy_static! { + static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.0.1").unwrap(); +} + +#[derive(Copy, Clone)] +pub(crate) enum RecordType { + Receive = 0, + Send = 1, + Chain = 2, + LastDerivationIndex = 3, + PaymentDetails = 4, +} + +impl ToSql for RecordType { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::from(*self as i8)) + } +} + +impl FromSql for RecordType { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Integer(i) => match i as u8 { + 0 => Ok(Self::Receive), + 1 => Ok(Self::Send), + 2 => Ok(Self::Chain), + 3 => Ok(Self::LastDerivationIndex), + 4 => Ok(Self::PaymentDetails), + _ => Err(FromSqlError::OutOfRange(i)), + }, + _ => Err(FromSqlError::InvalidType), + } + } +} + +pub(crate) struct SyncState { + pub(crate) data_id: String, + pub(crate) record_id: String, + pub(crate) record_revision: u64, + pub(crate) is_local: bool, +} + +pub(crate) struct SyncSettings { + pub(crate) remote_url: Option, + pub(crate) latest_revision: Option, +} + +pub(crate) struct SyncOutgoingChanges { + pub(crate) record_id: String, + pub(crate) data_id: String, + pub(crate) record_type: RecordType, + pub(crate) commit_time: u32, + pub(crate) updated_fields: Option>, +} + +pub(crate) struct DecryptedRecord { + pub(crate) revision: u64, + pub(crate) id: String, + #[allow(dead_code)] + pub(crate) schema_version: String, + pub(crate) data: SyncData, +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum DecryptionError { + #[error("Record is not applicable: schema_version too high")] + SchemaNotApplicable, + + #[error("Remote record revision is lower or equal to the persisted one. Skipping update.")] + AlreadyPersisted, + + #[error("Could not decrypt payload with signer: {err}")] + InvalidPayload { err: String }, + + #[error("Could not deserialize JSON bytes: {err}")] + DeserializeError { err: String }, + + #[error("Generic error: {err}")] + Generic { err: String }, +} + +impl DecryptionError { + pub(crate) fn invalid_payload(err: crate::model::SignerError) -> Self { + Self::InvalidPayload { + err: err.to_string(), + } + } + + pub(crate) fn deserialize_error(err: serde_json::Error) -> Self { + Self::DeserializeError { + err: err.to_string(), + } + } +} + +impl From for DecryptionError { + fn from(value: anyhow::Error) -> Self { + Self::Generic { + err: value.to_string(), + } + } +} + +pub(crate) struct DecryptionInfo { + pub(crate) new_sync_state: SyncState, + pub(crate) record: DecryptedRecord, + pub(crate) last_commit_time: Option, +} + +impl Record { + pub(crate) fn new( + data: SyncData, + revision: u64, + signer: Arc>, + ) -> Result { + let id = Self::get_id_from_sync_data(&data); + let data = data.to_bytes()?; + let data = signer + .ecies_encrypt(data) + .map_err(|err| anyhow::anyhow!("Could not encrypt sync data: {err:?}"))?; + let schema_version = CURRENT_SCHEMA_VERSION.to_string(); + Ok(Self { + id, + revision, + schema_version, + data, + }) + } + + fn id(prefix: String, data_id: &str) -> String { + sha256((prefix + ":" + data_id).as_bytes()).to_lower_hex_string() + } + + pub(crate) fn get_id_from_sync_data(data: &SyncData) -> String { + let prefix = match data { + SyncData::Chain(_) => "chain-swap", + SyncData::Send(_) => "send-swap", + SyncData::Receive(_) => "receive-swap", + SyncData::LastDerivationIndex(_) => "derivation-index", + SyncData::PaymentDetails(_) => "payment-details", + } + .to_string(); + Self::id(prefix, data.id()) + } + + pub(crate) fn get_id_from_record_type(record_type: RecordType, data_id: &str) -> String { + let prefix = match record_type { + RecordType::Chain => "chain-swap", + RecordType::Send => "send-swap", + RecordType::Receive => "receive-swap", + RecordType::LastDerivationIndex => "derivation-index", + RecordType::PaymentDetails => "payment-details", + } + .to_string(); + Self::id(prefix, data_id) + } + + pub(crate) fn is_applicable(&self) -> Result { + let record_version = Version::parse(&self.schema_version)?; + Ok(CURRENT_SCHEMA_VERSION.major >= record_version.major) + } + + pub(crate) fn decrypt( + self, + signer: Arc>, + ) -> Result { + let dec_data = signer + .ecies_decrypt(self.data) + .map_err(DecryptionError::invalid_payload)?; + let data = serde_json::from_slice(&dec_data).map_err(DecryptionError::deserialize_error)?; + Ok(DecryptedRecord { + id: self.id, + revision: self.revision, + schema_version: self.schema_version, + data, + }) + } +} diff --git a/lib/core/src/sync/model/sync.rs b/lib/core/src/sync/model/sync.rs new file mode 100644 index 000000000..2e707b828 --- /dev/null +++ b/lib/core/src/sync/model/sync.rs @@ -0,0 +1,212 @@ +// This file is @generated by prost-build. +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Record { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(uint64, tag = "2")] + pub revision: u64, + #[prost(string, tag = "3")] + pub schema_version: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "4")] + pub data: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SetRecordRequest { + #[prost(message, optional, tag = "1")] + pub record: ::core::option::Option, + #[prost(uint32, tag = "2")] + pub request_time: u32, + #[prost(string, tag = "3")] + pub signature: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, ::prost::Message)] +pub struct SetRecordReply { + #[prost(enumeration = "SetRecordStatus", tag = "1")] + pub status: i32, + #[prost(uint64, tag = "2")] + pub new_revision: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListChangesRequest { + #[prost(uint64, tag = "1")] + pub since_revision: u64, + #[prost(uint32, tag = "2")] + pub request_time: u32, + #[prost(string, tag = "3")] + pub signature: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListChangesReply { + #[prost(message, repeated, tag = "1")] + pub changes: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TrackChangesRequest { + #[prost(uint32, tag = "1")] + pub request_time: u32, + #[prost(string, tag = "2")] + pub signature: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SetRecordStatus { + Success = 0, + Conflict = 1, +} +impl SetRecordStatus { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Success => "SUCCESS", + Self::Conflict => "CONFLICT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SUCCESS" => Some(Self::Success), + "CONFLICT" => Some(Self::Conflict), + _ => None, + } + } +} +/// Generated client implementations. +pub mod syncer_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value + )] + use tonic::codegen::http::Uri; + use tonic::codegen::*; + #[derive(Debug, Clone)] + pub struct SyncerClient { + inner: tonic::client::Grpc, + } + impl SyncerClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl SyncerClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> SyncerClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + >>::Error: + Into + std::marker::Send + std::marker::Sync, + { + SyncerClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn set_record( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/sync.Syncer/SetRecord"); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("sync.Syncer", "SetRecord")); + self.inner.unary(req, path, codec).await + } + pub async fn list_changes( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/sync.Syncer/ListChanges"); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("sync.Syncer", "ListChanges")); + self.inner.unary(req, path, codec).await + } + pub async fn track_changes( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner.ready().await.map_err(|e| { + tonic::Status::unknown(format!("Service was not ready: {}", e.into())) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/sync.Syncer/TrackChanges"); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("sync.Syncer", "TrackChanges")); + self.inner.server_streaming(req, path, codec).await + } + } +} diff --git a/lib/core/src/sync/proto/sync.proto b/lib/core/src/sync/proto/sync.proto new file mode 100644 index 000000000..51e345f3a --- /dev/null +++ b/lib/core/src/sync/proto/sync.proto @@ -0,0 +1,43 @@ +syntax = "proto3"; + +option go_package = "github.com/breez/data-sync/proto"; +package sync; + +service Syncer { + rpc SetRecord(SetRecordRequest) returns (SetRecordReply) {} + rpc ListChanges(ListChangesRequest) returns (ListChangesReply) {} + rpc TrackChanges(TrackChangesRequest) returns (stream Record); +} + +message Record { + string id = 1; + uint64 revision = 2; + string schema_version = 3; + bytes data = 4; +} + +message SetRecordRequest { + Record record = 1; + uint32 request_time = 2; + string signature = 3; +} +enum SetRecordStatus { + SUCCESS = 0; + CONFLICT = 1; +} +message SetRecordReply { + SetRecordStatus status = 1; + uint64 new_revision = 2; +} + +message ListChangesRequest { + uint64 since_revision = 1; + uint32 request_time = 2; + string signature = 3; +} +message ListChangesReply { repeated Record changes = 1; } + +message TrackChangesRequest { + uint32 request_time = 1; + string signature = 2; +} diff --git a/lib/core/src/test_utils/chain.rs b/lib/core/src/test_utils/chain.rs index 47b9275a2..0a0073d58 100644 --- a/lib/core/src/test_utils/chain.rs +++ b/lib/core/src/test_utils/chain.rs @@ -9,9 +9,13 @@ use boltz_client::{ }, Amount, }; -use electrum_client::bitcoin::{consensus::deserialize, OutPoint, Script, TxOut}; use electrum_client::GetBalanceRes; +use electrum_client::{ + bitcoin::{consensus::deserialize, OutPoint, Script, TxOut}, + HeaderNotification, +}; use lwk_wollet::{ + bitcoin::constants::genesis_block, elements::{BlockHash, Txid as ElementsTxid}, History, }; @@ -60,7 +64,7 @@ impl MockLiquidChainService { #[async_trait] impl LiquidChainService for MockLiquidChainService { async fn tip(&mut self) -> Result { - unimplemented!() + Ok(0) } async fn broadcast( @@ -82,7 +86,7 @@ impl LiquidChainService for MockLiquidChainService { &self, _txids: &[lwk_wollet::elements::Txid], ) -> Result> { - unimplemented!() + Ok(vec![]) } async fn get_script_history( @@ -101,7 +105,7 @@ impl LiquidChainService for MockLiquidChainService { } async fn get_scripts_history(&self, _scripts: &[&ElementsScript]) -> Result>> { - unimplemented!() + Ok(vec![]) } async fn get_script_utxos(&self, _script: &ElementsScript) -> Result> { @@ -139,8 +143,11 @@ impl MockBitcoinChainService { #[async_trait] impl BitcoinChainService for MockBitcoinChainService { - fn tip(&mut self) -> Result { - unimplemented!() + fn tip(&mut self) -> Result { + Ok(HeaderNotification { + height: 0, + header: genesis_block(lwk_wollet::bitcoin::Network::Testnet).header, + }) } fn broadcast( @@ -154,7 +161,7 @@ impl BitcoinChainService for MockBitcoinChainService { &self, _txids: &[boltz_client::bitcoin::Txid], ) -> Result> { - unimplemented!() + Ok(vec![]) } fn get_script_history(&self, _script: &Script) -> Result> { @@ -170,7 +177,7 @@ impl BitcoinChainService for MockBitcoinChainService { } fn get_scripts_history(&self, _scripts: &[&Script]) -> Result>> { - unimplemented!() + Ok(vec![]) } async fn get_script_utxos(&self, script: &Script) -> Result> { @@ -186,11 +193,14 @@ impl BitcoinChainService for MockBitcoinChainService { &self, _script: &boltz_client::bitcoin::Script, ) -> Result { - unimplemented!() + Ok(GetBalanceRes { + confirmed: 0, + unconfirmed: 0, + }) } fn scripts_get_balance(&self, _scripts: &[&Script]) -> Result> { - unimplemented!() + Ok(vec![]) } async fn script_get_balance_with_retry( diff --git a/lib/core/src/test_utils/chain_swap.rs b/lib/core/src/test_utils/chain_swap.rs index 0f5b0d65a..82467d10f 100644 --- a/lib/core/src/test_utils/chain_swap.rs +++ b/lib/core/src/test_utils/chain_swap.rs @@ -10,7 +10,7 @@ use tokio::sync::Mutex; use crate::{ chain_swap::ChainSwapHandler, - model::{ChainSwap, Config, Direction, PaymentState}, + model::{ChainSwap, Config, Direction, PaymentState, Signer}, persist::Persister, swapper::boltz::BoltzSwapper, utils, @@ -19,7 +19,7 @@ use crate::{ use super::{ chain::{MockBitcoinChainService, MockLiquidChainService}, generate_random_string, - wallet::MockWallet, + wallet::{MockSigner, MockWallet}, }; lazy_static! { @@ -28,7 +28,8 @@ lazy_static! { pub(crate) fn new_chain_swap_handler(persister: Arc) -> Result { let config = Config::testnet(None); - let onchain_wallet = Arc::new(MockWallet::new()); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer)?); let swapper = Arc::new(BoltzSwapper::new(config.clone(), None)); let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); let bitcoin_chain_service = Arc::new(Mutex::new(MockBitcoinChainService::new())); diff --git a/lib/core/src/test_utils/mod.rs b/lib/core/src/test_utils/mod.rs index 9e71df43e..6f1a075b5 100644 --- a/lib/core/src/test_utils/mod.rs +++ b/lib/core/src/test_utils/mod.rs @@ -6,10 +6,12 @@ pub(crate) mod chain; pub(crate) mod chain_swap; pub(crate) mod persist; pub(crate) mod receive_swap; +pub(crate) mod recover; pub(crate) mod sdk; pub(crate) mod send_swap; pub(crate) mod status_stream; pub(crate) mod swapper; +pub(crate) mod sync; pub(crate) mod wallet; pub(crate) fn generate_random_string(size: usize) -> String { diff --git a/lib/core/src/test_utils/persist.rs b/lib/core/src/test_utils/persist.rs index 3768f60eb..6bdf1927b 100644 --- a/lib/core/src/test_utils/persist.rs +++ b/lib/core/src/test_utils/persist.rs @@ -1,6 +1,5 @@ #![cfg(test)] -use anyhow::{anyhow, Result}; use bip39::rand::{self, RngCore}; use sdk_common::{ bitcoin::{ @@ -10,11 +9,9 @@ use sdk_common::{ lightning::ln::PaymentSecret, lightning_invoice::{Currency, InvoiceBuilder}, }; -use tempdir::TempDir; use crate::{ - model::{LiquidNetwork, PaymentState, PaymentTxData, PaymentType, ReceiveSwap, SendSwap}, - persist::Persister, + model::{PaymentState, PaymentTxData, PaymentType, ReceiveSwap, SendSwap}, test_utils::generate_random_string, utils, }; @@ -134,25 +131,28 @@ pub(crate) fn new_receive_swap(payment_state: Option) -> ReceiveSw claim_tx_id: None, lockup_tx_id: None, mrh_address: "tlq1pq2amlulhea6ltq7x3eu9atsc2nnrer7yt7xve363zxedqwu2mk6ctcyv9awl8xf28cythreqklt5q0qqwsxzlm6wu4z6d574adl9zh2zmr0h85gt534n".to_string(), - mrh_script_pubkey: "tex1qnkznyyxwnxnkk0j94cnvq27h24jk6sqf0te55x".to_string(), mrh_tx_id: None, created_at: utils::now(), state: payment_state.unwrap_or(PaymentState::Created), } } -pub(crate) fn new_persister() -> Result<(TempDir, Persister)> { - let temp_dir = TempDir::new("liquid-sdk")?; - let persister = Persister::new( - temp_dir - .path() - .to_str() - .ok_or(anyhow!("Could not create temporary directory"))?, - LiquidNetwork::Testnet, - )?; - persister.init()?; - Ok((temp_dir, persister)) +macro_rules! create_persister { + ($name:ident) => { + let (sync_trigger_tx, _sync_trigger_rx) = tokio::sync::mpsc::channel::<()>(100); + let temp_dir = tempdir::TempDir::new("liquid-sdk")?; + let $name = std::sync::Arc::new(crate::persist::Persister::new( + temp_dir + .path() + .to_str() + .ok_or(anyhow::anyhow!("Could not create temporary directory"))?, + crate::model::LiquidNetwork::Testnet, + sync_trigger_tx, + )?); + $name.init()?; + }; } +pub(crate) use create_persister; pub(crate) fn new_payment_tx_data(payment_type: PaymentType) -> PaymentTxData { PaymentTxData { diff --git a/lib/core/src/test_utils/receive_swap.rs b/lib/core/src/test_utils/receive_swap.rs index 0685249a6..93bdee61a 100644 --- a/lib/core/src/test_utils/receive_swap.rs +++ b/lib/core/src/test_utils/receive_swap.rs @@ -5,13 +5,22 @@ use std::sync::Arc; use tokio::sync::Mutex; -use crate::{model::Config, persist::Persister, receive_swap::ReceiveSwapHandler}; +use crate::{ + model::{Config, Signer}, + persist::Persister, + receive_swap::ReceiveSwapHandler, +}; -use super::{chain::MockLiquidChainService, swapper::MockSwapper, wallet::MockWallet}; +use super::{ + chain::MockLiquidChainService, + swapper::MockSwapper, + wallet::{MockSigner, MockWallet}, +}; pub(crate) fn new_receive_swap_handler(persister: Arc) -> Result { let config = Config::testnet(None); - let onchain_wallet = Arc::new(MockWallet::new()); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer)?); let swapper = Arc::new(MockSwapper::new()); let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); diff --git a/lib/core/src/test_utils/recover.rs b/lib/core/src/test_utils/recover.rs new file mode 100644 index 000000000..00e7aa39e --- /dev/null +++ b/lib/core/src/test_utils/recover.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use anyhow::Result; +use tokio::sync::Mutex; + +use crate::{model::Signer, recover::recoverer::Recoverer, wallet::OnchainWallet}; + +use super::chain::{MockBitcoinChainService, MockLiquidChainService}; + +pub(crate) fn new_recoverer( + signer: Arc>, + onchain_wallet: Arc, +) -> Result { + let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); + let bitcoin_chain_service = Arc::new(Mutex::new(MockBitcoinChainService::new())); + + Recoverer::new( + signer.slip77_master_blinding_key()?, + onchain_wallet, + liquid_chain_service, + bitcoin_chain_service, + ) +} diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index f254c231e..14d69021d 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -13,6 +13,7 @@ use crate::{ model::{Config, Signer}, persist::Persister, receive_swap::ReceiveSwapHandler, + recover::recoverer::Recoverer, sdk::LiquidSdk, send_swap::SendSwapHandler, }; @@ -21,6 +22,7 @@ use super::{ chain::{MockBitcoinChainService, MockLiquidChainService}, status_stream::MockStatusStream, swapper::MockSwapper, + sync::new_sync_service, wallet::{MockSigner, MockWallet}, }; @@ -55,8 +57,8 @@ pub(crate) fn new_liquid_sdk_with_chain_services( .ok_or(anyhow!("An invalid SDK directory was specified"))? .to_string(); - let signer: Arc> = Arc::new(Box::new(MockSigner::new())); - let onchain_wallet = Arc::new(MockWallet::new()); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?); let send_swap_handler = SendSwapHandler::new( config.clone(), @@ -83,6 +85,13 @@ pub(crate) fn new_liquid_sdk_with_chain_services( bitcoin_chain_service.clone(), )?); + let recoverer = Arc::new(Recoverer::new( + signer.slip77_master_blinding_key()?, + onchain_wallet.clone(), + liquid_chain_service.clone(), + bitcoin_chain_service.clone(), + )?); + let event_manager = Arc::new(EventManager::new()); let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(()); @@ -91,6 +100,10 @@ pub(crate) fn new_liquid_sdk_with_chain_services( let buy_bitcoin_service = Arc::new(BuyBitcoinService::new(config.clone(), breez_server.clone())); + let (_incoming_tx, _outgoing_records, sync_service) = + new_sync_service(persister.clone(), recoverer.clone(), signer.clone())?; + let sync_service = Arc::new(sync_service); + Ok(LiquidSdk { config, onchain_wallet, @@ -99,6 +112,7 @@ pub(crate) fn new_liquid_sdk_with_chain_services( event_manager, status_stream, swapper, + recoverer, liquid_chain_service, bitcoin_chain_service, fiat_api: breez_server, @@ -107,6 +121,7 @@ pub(crate) fn new_liquid_sdk_with_chain_services( shutdown_receiver, send_swap_handler, receive_swap_handler, + sync_service, chain_swap_handler, buy_bitcoin_service, external_input_parsers: Vec::new(), diff --git a/lib/core/src/test_utils/send_swap.rs b/lib/core/src/test_utils/send_swap.rs index e2f559b9a..226d33c69 100644 --- a/lib/core/src/test_utils/send_swap.rs +++ b/lib/core/src/test_utils/send_swap.rs @@ -2,15 +2,24 @@ use std::sync::Arc; -use crate::{model::Config, persist::Persister, send_swap::SendSwapHandler}; +use crate::{ + model::{Config, Signer}, + persist::Persister, + send_swap::SendSwapHandler, +}; use anyhow::Result; use tokio::sync::Mutex; -use super::{chain::MockLiquidChainService, swapper::MockSwapper, wallet::MockWallet}; +use super::{ + chain::MockLiquidChainService, + swapper::MockSwapper, + wallet::{MockSigner, MockWallet}, +}; pub(crate) fn new_send_swap_handler(persister: Arc) -> Result { let config = Config::testnet(None); - let onchain_wallet = Arc::new(MockWallet::new()); + let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); + let onchain_wallet = Arc::new(MockWallet::new(signer)?); let swapper = Arc::new(MockSwapper::new()); let chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); diff --git a/lib/core/src/test_utils/sync.rs b/lib/core/src/test_utils/sync.rs new file mode 100644 index 000000000..a4ab84e3a --- /dev/null +++ b/lib/core/src/test_utils/sync.rs @@ -0,0 +1,168 @@ +#![cfg(test)] + +use std::{collections::HashMap, sync::Arc}; + +use crate::{ + persist::Persister, + prelude::{Direction, Signer}, + recover::recoverer::Recoverer, + sync::{ + client::SyncerClient, + model::{ + data::{ChainSyncData, ReceiveSyncData, SendSyncData}, + sync::{ + ListChangesReply, ListChangesRequest, Record, SetRecordReply, SetRecordRequest, + SetRecordStatus, + }, + }, + SyncService, + }, +}; +use anyhow::Result; +use async_trait::async_trait; +use tokio::sync::{ + mpsc::{self, Receiver, Sender}, + Mutex, +}; + +pub(crate) struct MockSyncerClient { + pub(crate) incoming_rx: Mutex>, + pub(crate) outgoing_records: Arc>>, +} + +impl MockSyncerClient { + pub(crate) fn new( + incoming_rx: Receiver, + outgoing_records: Arc>>, + ) -> Self { + Self { + incoming_rx: Mutex::new(incoming_rx), + outgoing_records, + } + } +} + +#[async_trait] +impl SyncerClient for MockSyncerClient { + async fn connect(&self, _connect_url: String) -> Result<()> { + todo!() + } + + async fn push(&self, req: SetRecordRequest) -> Result { + if let Some(mut record) = req.record { + let mut outgoing_records = self.outgoing_records.lock().await; + + if let Some(existing_record) = outgoing_records.get(&record.id) { + if existing_record.revision != record.revision { + return Ok(SetRecordReply { + status: SetRecordStatus::Conflict as i32, + new_revision: 0, + }); + } + } + + record.revision = outgoing_records.len() as u64 + 1; + let record_revision = record.revision; + + outgoing_records.insert(record.id.clone(), record); + return Ok(SetRecordReply { + status: SetRecordStatus::Success as i32, + new_revision: record_revision, + }); + } + + return Err(anyhow::anyhow!("No record was sent")); + } + + async fn pull(&self, _req: ListChangesRequest) -> Result { + let mut rx = self.incoming_rx.lock().await; + let mut changes = Vec::with_capacity(3); + rx.recv_many(&mut changes, 3).await; + Ok(ListChangesReply { changes }) + } + + async fn disconnect(&self) -> Result<()> { + todo!() + } +} + +#[allow(clippy::type_complexity)] +pub(crate) fn new_sync_service( + persister: Arc, + recoverer: Arc, + signer: Arc>, +) -> Result<( + Sender, + Arc>>, + SyncService, +)> { + let (_, sync_trigger_rx) = mpsc::channel::<()>(30); + let (incoming_tx, incoming_rx) = mpsc::channel::(10); + let outgoing_records = Arc::new(Mutex::new(HashMap::new())); + let client = Box::new(MockSyncerClient::new(incoming_rx, outgoing_records.clone())); + let sync_service = SyncService::new( + "".to_string(), + persister.clone(), + recoverer, + signer.clone(), + client, + sync_trigger_rx, + ); + + Ok((incoming_tx, outgoing_records, sync_service)) +} + +pub(crate) fn new_receive_sync_data() -> ReceiveSyncData { + ReceiveSyncData { + swap_id: "receive-swap".to_string(), + invoice: "".to_string(), + pair_fees_json: "".to_string(), + create_response_json: "".to_string(), + payer_amount_sat: 0, + receiver_amount_sat: 0, + created_at: 0, + claim_fees_sat: 0, + claim_private_key: "".to_string(), + mrh_address: "".to_string(), + preimage: "".to_string(), + payment_hash: None, + description: None, + } +} + +pub(crate) fn new_send_sync_data(preimage: Option) -> SendSyncData { + SendSyncData { + swap_id: "send-swap".to_string(), + invoice: "".to_string(), + pair_fees_json: "".to_string(), + create_response_json: "".to_string(), + refund_private_key: "".to_string(), + payer_amount_sat: 0, + receiver_amount_sat: 0, + created_at: 0, + preimage, + payment_hash: None, + description: None, + bolt12_offer: None, + } +} + +pub(crate) fn new_chain_sync_data(accept_zero_conf: Option) -> ChainSyncData { + ChainSyncData { + swap_id: "chain-swap".to_string(), + preimage: "".to_string(), + pair_fees_json: "".to_string(), + create_response_json: "".to_string(), + direction: Direction::Incoming, + lockup_address: "".to_string(), + claim_fees_sat: 0, + claim_private_key: "".to_string(), + refund_private_key: "".to_string(), + timeout_block_height: 0, + payer_amount_sat: 0, + receiver_amount_sat: 0, + accept_zero_conf: accept_zero_conf.unwrap_or(true), + created_at: 0, + description: None, + } +} diff --git a/lib/core/src/test_utils/wallet.rs b/lib/core/src/test_utils/wallet.rs index cd7a6c254..bf828e7f6 100644 --- a/lib/core/src/test_utils/wallet.rs +++ b/lib/core/src/test_utils/wallet.rs @@ -1,22 +1,33 @@ #![cfg(test)] -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use crate::{ error::PaymentError, model::{Signer, SignerError}, + signer::{NewError, SdkLwkSigner}, utils, wallet::OnchainWallet, }; use anyhow::Result; use async_trait::async_trait; +use bip39::Mnemonic; +use boltz_client::{Keypair, Secp256k1}; use lazy_static::lazy_static; use lwk_wollet::{ - elements::{Address, Transaction}, + bitcoin::{ + self, + bip32::{DerivationPath, Xpriv, Xpub}, + }, + elements::{hex::ToHex, Address, Transaction, Txid}, + elements_miniscript::{slip77::MasterBlindingKey, ToPublicKey as _}, + secp256k1::{All, Message}, Tip, WalletTx, }; -pub(crate) struct MockWallet {} +pub(crate) struct MockWallet { + signer: SdkLwkSigner, +} lazy_static! { pub(crate) static ref TEST_LIQUID_TX: Transaction = utils::deserialize_tx_hex("020000000101ad4f1152b3257e081c6f4bc67eef57bae997a152f6e4226454e24b49a45798a70100000000feffffff030bf5295cecc24c0aeb1813e12f974a7a4696a408bbcf4eda4d0270379e5ebd4ecf0934662cadc03364cf7729a25f733cc8999e0f2b12d9ccce3762bd170950e052a103733b0357c226e9b8d56d915ff369f3d10b1089875e358a6453e5a0cf0be0da9f225120110f1e9c0f2dba901ef79b45ebc181d87d86498798f37993f199b64e486aaa980bffc8792431bb1bf5a8a7e307d5fd0883467e2bff3473e1787cc2cf9c7103db6208518b412cf79f6d72e522de8b9c8d208ae00cd805d354c315523002bb2e4520a703e373df7c58aebcc4920f722ba5049f7ff2ec345021bbdace3a86bf8d329a552816001464a066ef48f5f3e1d486b1bac4e928e01ae93ad001499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c140100000000000000fb0000a74a160000000247304402203fc6f037451c1c9b38b90cb788022fa051da1812f310461847cef98583b8d41502205781bf725787978dbe55e343334fc30687b9caccfc249561fd1e529ba987392f01210273aa2eda3cd21dedf796e3058e312c50c8275e674e765ea01c5fda74a288f3b30043010001e78cc52b1ffb6bab378a17f05de5531977b6017cf64a02df2c9b8ba19a376d0828de10fd9cf89db899dbec237feda1c02018f49df8c90a44d8adf63b1c8186ccfd4e1060330000000000000001885baf0128d296357c2c77e7abe462d33c7dc4861519e55d4edf7c4c5ba12fe947457349fd1f0a8b3046df64b1cf4aed94a5b2286a7ac434a34814b6660c8f347b77f06d2daf36cadcbc8c796fbb515c9ad38e6bcab4a66b346a3a1013886341e8cb1c92e7e25029b49091d4feef8d06516db1255a65f0eb55173b35e9b9f16a3825f7a7ca58988bbd9a0d600af8a806c506f911eb78384918d8a230f7dca14af328c84e6e6699679cba3a75cf942ff5d2e45342232377e5912c259e91e12331e3c0a3ec946e812f4dacd1717336633ffa005f29a81cf34b094ef9f74d4941a7b2b21ea6dd8367edaac115de9d2cbf1ea612377bcf784f3c9922a7a55e957734e67d2ed2a9d368f3d07be5fbe89ad3a67e6b53ba14449b2e74ee6b7f8aff85e39a5f09bbd53736a70ea38d32d6de580d723a25dacd8e68b3101cbfc9ff4e98fae44a1cc2bbbf8d5d3248a5157a15d17855a38bb4fa8f13aaaa2d1d3559f6b325c5e2a9b03f7606e40cc899e46534684acab1d3a6037bf2aa3c68f94fb4dbb63619e55d34d966bf596928bfb334191e486e3881096377a18e8c607ca27716cd3b342d761ef2d57d8faa4b833faa0ae449997eb807ddaccbbee97e87a2467a680df95b92d265e20183078976e3091b11330ef23e28fd713f67cc22b9e7752bacf7a01a09952b6e98bb80e7d6874320431a2194e9b5cd3c04316acad19e16201ad3d022eda5cace187b5af331922f9a60fff7468e148ab6164a14c7afa93fde9366d57e82bc4c0e90dea9000d21d6fc731b072fd83781fb6447fbf5c924f5feb70991aafe7ae79aadd436b42fa8deb696d13e8df31e17fe61ddd4d208ff5285347c65ebd0dee50f449ebe083312aec16d5044eaeceea10f749999618ad6cee5010daa0ac0208aff1dcefe2facf533a98184d563790e1b9b764ba8cb37f714f4e5d20801661d49d8c72634fab988584b6f091e18164d388f92b281d0f930b49665a4cd4b996968e9177d5f192d98c2eb930eb26cb024b09d338e24aee44a10e26fa7f96d1210931fe9999120df590f29e0eb279e3054abf330a864e814d56877c155eee1aeb9f40f6550bf472dc4860ca7ca685111d98bd57719a09698e4f610bb7bdf2f3d49729c9b86f095050a48baa000efce2c7bb77f476fba3ae01627337f3626f02771ab370f718208e0ae949c5de969f65456bfc476014af96af1a746aae5ddf1a9d566f8877367aaa906c2c4388bdd1f99993324d6a8391186620ea6e5e7b277ed3ecfee5094d69cf683a61e01a42bbe3bab92faefc18728332984a075598543ac3719a47f0387fac23e6f16badfbed8490f98dce7322382b6a8a08b5c8ebe819f5ed4208f4918017b37b23b32e55494ce359069549bee525713ab377687441ec8b6b3213923437d1acb207377992e753893d1b2a4cbbb3a3ece2a3090178c162f74488699eccc47d62cfb7c985145558fd8076c8075d4318eb9b12f5aeac1a9c7c5ee1d33681b5bb21bc3748b38bea4ac3079282226658fbeb124237aa3c1f317420fb16d6ba551a0739d142f47254a42d7068f2982261b67e46403b5287e4b60f2e3d826f1e527896d9c7748fc9705098716ef670ebc43a204fc19def123610d937f76bf63013e7d818924edec1e81d5f695f80490509e927804c054a2f3c049494402d38e55e693ddd29fb77a87639f77ce58ba951ae91f6587f802f8bb43ce5f2fa73589bcf43b68ca3a8fd9d057ef87d4c8cf7dd29982ae3c288d2f72c8428526daa1d64d9771cc546d275cf87053d045fea2c741d66077327e1a3d5ddf209d9142a7c415d6602109346cb0585b3e612efeb0ac41ff0608e50d69fd66dac26038e3a9436c9d7261ab65bbb49563532383b25e4d96e7f89724fc2e901b79b45e2c7c074aa2775bcf3e49694588ae39e9a6920ff53c94b9a540717f3f3abbf2eeca76fdf7c20f2e4a23ef08629ebf7cfca6c293726f7d59e2c5fc03ea775ae5fbd290e5498bb3d0f8d5e615dee582f74d04c70e67fc612da8d0ee0074e8b3f5e7de4192dbc40dcbb10ad30fd33224b9bb39002be2a407c492662a98d867c7d122bb75809c401a7643ab32722470b919af6b739f3222ea27ba0fececd85285f840b209e266078968fac4a073270b3769e7aef0724ef82468bd4d38cb8d2770156523d6f2ed9801533167951e680711b69a9d18767d0a6dcafa6e4d638b0e19efafb348098a73ba8479cee72f0a7f83e8aaf3d45be4458feef8787e3281d19e1fd84dad83fcc0eba6a4538255769512a1e3cea199f59aa847565c631f26cf47cf1d0fb71392e2dbd3c4611a42d8f437cd7c0dc3632c6c157ea9164e1ffa1c4c45dfe740db354e8b32d73a9f93e355e6a2aef00ab7e906429b4f514ceccc7bc2622a3708441ee1d9a43a1af55bbda78ab980afa05cb14d5504060753f7f595d50c07a9b496a1ab57d630146eff658be229859574ac1be4ab652d6b2039d5b393aa03fdc882c21790db4f6e0fe30c16268100dd55695d0dfc6a91c78a9f2cd09fbba851bae901eb22e946376b8c85617f7114fcca7e65e0a463b9e789ca85be58b9ea65c7008e9b4794ae1422228841c5f5584da495d9061ec7860effa862135daaa93bd26cc5c2ab46bb44404b16e93255ede18e00b7c61ea13a543cf2232e61aaa9adc0e20db069797e35458e9f3f3c6647eeab65cbc9bd5c0848dba44bd0779fdee363e7944bf3b7961b28bb84f18e4317cd19aca0b27295060500d8d119e1e6af3e2179ca990ce93529d8726963968ada3d95761a290053e8aecc91167c67d44762cc23c524af84e0f70d493190d2ea3ad734bcf5ddbd1ecbc6cb0ace2fb3215d7d0914ee1dc111e39559a3cc4ac1177d295ec362d2285a3a8b6b0935600a1ab5bef1c2e31b18c187235dd051be4bad4011ee0abd07539d40fd1649c0f125541c937155cffa90ce018cc57a94d2ea83822356a0f085383df7f4f9664224d0ace4bf0f6d3a27a7849bed2fe0ecb5ed5ddd39aaccce9494e11d832d351e7f4b212936fbb265e00a856a413bd4e6e18e0140817ec6a31d34feb29cebf59efd0736fdf6362a18fe58a089032cc7d5a1a8b3f9881e416235e1952eb2cbd1a3a27ba5ff664227d6537d3c331f565f4c3d2e0f19f1f42b8ad1ed1623cd8fc3e93916accf433ffd30aceeb249b259ab0338e3cfdcb2e8755ca294a22d28344ed1fa8450631bfa6098dae5453ec1ea0cf78768287fe43a068492070dabc826f77ffb5bd3b305cd7d9e0f4128468c160de05fa339172dc6cc906b6da874dc5abf123683074f0c05a4531f3600a6c1a65d78e622582b04cd71fc0a2eaa2e811e8dc0cd00d89c4d9071043eae9e124c131619e37b6d68b0aa1e167bb98f9dec19463b2e1930a39fb67e07a15a7e886a5e62f857007a35d4d687487b693681592d71ab50889543faba7c9047a4c27e47de00bbef32f3b64fa9d0a84712d980423e25632fbdb9e37aa22068e3a0725ba7cc63f5317e8634b87c0ad7ab530ebc2d7899c519964f3570f75f6092a42a26e7d5085326e23b25ef0414126d9168c4c74225e4642682ba58aa459571e3ebd508a451d7387deed43fe10d384eaa6c602c3a4f3a60fe9e08cff85bb031f796dd446d7a2108de04bf06efd7680017311b435bb0d36ccb5cc8d1e85aa6980b278d1bb35f0f9f06cc76ddd9b047089108be63bd628483b026ced478c3aee50e3cfff03d2e0d0e82b797fe217c4fc11e5a510798521a41b22a305c5340c052470755a4a4b1af2e1931877e22fd89238c6712bf1823ed84414e027bf818d09ed493136f8bb5de4c190046f3290c18ac648f6ece727db7548b7916cf566bfbafa20fbeb5d1fc58ffddfb0bdeb258f1aa5979846cd53022946855666ffa061e58c7fa3be9ddd922868779e54c9775ea8c6f8ef9242172d85c097527cd1eeb55bd24dbcde753963b8d33c3e3b835e06286b1af566dfd2b4a0f50f5a713c810103229f9b95a9ecf0fdfc31c4b4b67e65c3ceff9472fe1e015fb08fc38b16d02b8c5f6d31f99bb83bb28d46e8ee7518d006ca6dab24dc35a955e61a0aa5e68087260ee1068776199b54d62b92ba952cb104189e7e04c7379abd702c96a6b92df8f02bd79bb3f93fb5c90827f2216ba723fde14ad8068a50a8f96690f3bdbd06d6004cac2dcb5136a397a5757f69af4e5bcc9ef450566d249d8e28547996c7229807e79741e8c0d8e30b9f42ef89dfbe5706d4088ce461fa7e80e383e1fad7f0fc650993dcc97a307392aee96759df1e026a8f56c0bdef2df064f6849b7049e2d74937e69f444fecbed5d18ec453764a411a5d22b57ce46cd95c95c8c48ac32bd0bce6bd19b2fca228439ddfdd62b51decc20f4340095549bca8520a725a4c669586508f83a49260993c60c0787e9ebc6a97fca288a2b3b0ad1244c508d1050fbfc2b28028d644aae413c495485db5b75323d230a5f4693fbf0ef9f134fbccb452231de5f28271b49c059193f46f1ba16075a2092a3ed171cfd8868fabd21f3d7c42cea5276cddb548236c3943f57fdb852d5ebf8a1a5bdc08f2c6acd6f15c97a3396032ebfbbd3d91442d187847bda801b84edda5687f3d49a14f52e7a3f7baf51a27b88be94b968875b3cd100f7b009ba4da6400b7473cde21a620e1961515757c34b746c5d707ba98c8f54c2fe3c3e11ab44c8aa30485476d43c70b0bffe44b5125c1aa0db1a4298f5e317ea6abafaebca625fbd61d2c7001373cf83893df5cd6cfff6d1505bb65892706f6a65bf3e31e2d413fffc474d7a016604760944a592078bd1d02d677a2e0e9bea91c13ec84f2dfb6dc072ea864b88f2c6cf7a570672f9e316e8cc92ab2c08fe1c068c11c3a154260f3ea0d59eb79d12e198d49da5bb4c0d1ef21ea779e5e185e896a61ad344e03b092ba094ca8d5bbfe95afb1e25f7d2f0c9334740d98baf6c73c9e1a7a1632a8a5646bb045133c3fde473352f1e9fe2f38ce5da8c029ee9a4f31064d97d8b8afae2d813d796ee7914bcfc0191f9575688d199060b11991c5ef38306a7cb2e86bd2f6eb2f7f42f9418d48e7cfe682bb6a6b9f0aa26bb9b6684bf98581e32660676eca8151ddde3f35e662efa044c4f6fbc560b6841cd8231f7be7674eba149442e131953bcf76330f2452c5c4b1d7539ba62a5ddfcb77b2e281f1e28a557a35df9484527c01e3b032570225f95fff811d37babb3349fe1f3efdc8ae766dce4e24c58967579ddc9009b70281e9c01c5651a5cd55ee95e1ccac86286dbdb7ec912a978c68ec6a22daf71073d7280d20d7e6d6072179556d25d31cebce0a557a12093925cdc99889aec6b70b8ce788c420df0e3f03455f8558955899160c7010a6bf01039ed8ffec41a4072f8f1de955e5247c39de2c6e51c90710a46a1dd5e3f3a6d2f95a7b302277483f1497307a29c25100f4caf44d00da52f9ab44292af4485457d388066a14301f6ba64abf07040d14cda6b7ceb1a210d5e8014a712371092ff17ed1f713c4236e3b54bb00ae6c2c3f39c11645ab9edbb5707aee02aa1ad44883f7408491653bc0aa2fee5761425a645d839ced27a80167af762ef97018de64709552c26228897733632744446b640ae704bc349d1d280108b4227eff15f914b3ea58c56d226c64809283ff37931973e78b305609be2f62da8f2836871af1bce9b7cd844130b9bac005276b3d2d737ecdebda3208a6adf4da4c8a3f91a0a4d82c7ffe3a3deeaee7694aff375ca07f0a2ee46d2cdf1eaac43f1a3665843468cc6d4a4ee20011bef4224239e7a41c62cf975dd1eeb60cb1a796f9785fed3a9dcfae80faefed7d436f72db8c7482f3c3a593c2ce6f4f423ce71161d1cbd441025ca18789b9c36b22b7bef9b43010001297558ec8f691f65cc76f4fd40acada6174138c6385451f665ec49b2334ccaf3d78fe140971fe91f59ec675676c66b2368e774277309c607c3b294209050f20dfd4e106033000000000000000115259300d89c137c87dbdf6e59ad3431390fe57f76fd6e330990f5a1b3b642b2bf65cb76aeef001eadae2983192508eb32976ad4db2449da4d28c049e1457f8935ee30791dc50be9cc264d99dfacc8c2b1c8f2efbd2d47f451eb82e48f42cfcba971df515c45681993a373b1ee08e8bdc278bf4652d6304b74fe8d19c763ab3d78db6288b086094152de988256d855f516df4a7eb6b06aff79bcd91830dcb3062d1a4e92a50a416847c04d9d26f36b4d02c37bac43876a7d4a5f02ac1e35fc7e5a8f44df18b1baa1965822fd2348a242b915e818b534106213e685ba1f212c3b77630b3be5f47baecf20092d11409ca27a0c690d6f2d0860e19ff730042993740880fb2eaa5afc32a55b8c4942272e146caee98ba0c8265f9e0ede2728cfcb573406985064def6ec2121b492673498bb556167e5011c9113cdd1ca3852ea368fdcc8dde005a9eb8c8dfeb171e316f3e02e9534f40e482bf93cb51c337022acfe68f8d3bab5374911a56c04c477b9f0fc6796e972e91d2443712f56faf69b3d5348ad0d01ef88272782a1d3acb6ca025bca8905cb5c08b6079ce9307be3755ea6599a2475dc749c0f5078163a6e7aea6419c50e5dd30072a767f5e21809741299b438fecd8b454308f921d5a4157edefbfda18a0be54fc6564972efc04c8ad7308c30fd347d554ad364687e80000c22ac0979971e4981f4b09212f558a84383b2222a7f775ca54ce45238d3a5538be7f21c9f20835a1d0b47bf3dfe01f84848a2aa553efb20655ff41353ca4a22fac61f5a7d119069c2895b0cbc640aa4e997b8f1cb0b726e178b30cace44ec171c3383e0a2d36f9e0666c4e7f87a06455305bdce2a5eee01068a3e863af91e897818cdb4317ce661ab5cc108b80119635b8112771b203d6e21f7649596ac1bfa7f5cd65be756886df4be6e65f89d08d3736577d2dd53610cb43adbef691c77f45b831897e88b03bd33f0e6deda24d439b89e90ec25b632a1476ee04f5d5ed475677e21b01c00672fe01b7631ffb29e2c9c4e9e7f1ae5b9dbc4def83f41023638747f84be5e70abce472daa838d40de4a7f600d23962a29774f46c28b6f541c78c57f4eb53ba734d46aa45b70626876d984e02fe11a904c744f52d36b343467116ca2d5ac36fbe76232f9638241bc0ed7ed1fece77f929df3cda759dad5fed2b773c4577fc8e04475dd020f7d1252f776a89a110b9919c4234d9044a2da7bc146b43e03a7c7d9887f12044a2ca61c15bfdc5d537067e6d00c203a47d132e53aa8ac040bcea77cbcc819be86cae3d1a8aaa7f7bc2832f69effcfe567c13ad7f2fb82a63a478bfd1f6a08b811062be98252ff8899a4a92366ffaee8de837fb75398f27615b2ba0c4ec419c73fa0e6fef2c3d6de615cb2958f4daa68324f93ba12dc7cf325ed59d45ad02f1ca3ab5edd99bdc19b366dea19ec510e12a22fe1533ab8fef5a569035f09f0afeaab3960f51ff6c03b1e192203e098b7b841855e044c6b5c26818deca6f55fcbda26fbdc23225b720f175c089361d695c4e1066006a9c60e457fac07860a11772eee7b1b69cdeb2358f15636adea630406943e40365e9fae820d579098a3b914aa260ef3331b2b875f1cdbddaf8da713b68229e2dd521a6c0408e2a883cbb7c2ba1deafaec861b35b6097d33ccd87aed7de5223dcb714d6fb33f6ca506bde4abef56898649aa2054f27fe6d8604bf66dcd271de6ec4487b4032cc5053e09f786f6fb09d9e29736e5be0af994f81e704720a88aab6240087469f3160146f565d199cdd3ebb93b49faf67d9fae7e683c2307d839a79b2c3997a1a71048fe5609913904b02de3e3e02d990770c90719d4aed6a06e8e42ae6e737fd7436ba332204b31575039c39f8d829f3893c26f7c53cf5b1eb8203761bd130be460568812708ced7ba444963b19e8852c33026e31ad88e52d443d29aa575a730730fd3abf8fba4a0bf5e8af64ba4de9255e0cfb16bf57e42aa1d21766c610fd0838dc1c8ed81b03f837000f538b8c68615499a2fdedb3f07eac95a908732485b79362ba94070c8459106c4f53dc2615e6f618530c37fc5492e0a7e362e117be4c080caaa34c41d3b5168a29b3e89efd06e0e7b3cf19506383d06af41337c0eb6c832a6f8751df091e4461721fbbb01857df41bd17110d4016122a769e37039c1cc684947053d0ac66db1e08cba0130a9e26bf2730d2cba7b7a20eda686a68749bb04b38fb28ae1d1c3a9f37422236e33a7814f61571f9fbd579b095878c4688f22cb14da47c0dfe3b9b385aba06ede469562bd18f19f4e010fc459e4fe99d47f7796fe9fb3fe39886f843c90a108b1d75a4ec61091771cb9334cdc2b85f7ee282142c22c0d9cb54c64759fdcc28547d215c54d1f5d98b266fc5c066811efe79fae460067bb7b7faf150390b9c8481fc28086a4f27e766b426458c39d8d36aa3c9fb4dc5fe25fd651aad1ae43b07dcd77b195ab4e226266fe991bf5b40a0a00992f17141beabee191625199cc8f508a6214e9e0bbf6854313b86222fd50f23a320f27129b0a87d9c71377d7c9ff42df71171353f49ea1662232a504c0121aeab611910eb2d86fdd108fba6fc01684f90ee8290e602f59278d902f3df95012f3b3988d1d78907625693cb811eae25b1aee27525dd89ccfe664b914187ac9a0c194820db231dee5c0d43f4673ef7a110b80c3dbe2c4cfd450ad65934ddae4daa2921bf18fd6ae6c63c767b6eae8fb08bb7b33b0f6563297f0d454f332ea33bac6899d59c8eb17a90a5290e6db44f08c070c3b0b2e8f13e5099d37f1704b3cdf6bdeba51142ec8461996b09921bee59924fea18f2b8cd033b2be2f90b3ea6cbef1ff8aa14583d5c983a44f9e064c65aed45c61879a9d75b96ee6f911900ae894a47b8e22a51f1d66f5091000d6dfe9219c295951f0adc3f6764cd1109f411380ae303af505bf9347238bf0910778983b2900950d9473ceb764a119324a3b323cfcbc661c052840c540c6c595ed7377021a8742fc7835b2cdaf21988f27d9e210f7748c4ac54b758220dc085333291f4767a653a85d7a04541c34557a2e043ab1786b9c41059f62d84ef3d85033fd7581ef5bcc119a81a37a68e02a7ea23f24762ee9751318c1e232536671e0993dc8819584ff4d95fd1aaa539570dba994332dc82842ebcce0806fbf85ce1e1f051b36885a4256b706e15d3844084c70cb64043d5eaefffe662e1eb700e1d58e5908768671e31e8b45f824cc291a1796fbc8abed04515e814db4125313a007b8d1923be44016dd5e8217870b76485d2aeb4991db51c6a1ba2e502f328a90ace24d5c22cdca0f43d980bfba05d4ace2a6c7af71ab809f175d1df3a0f4b344315a8f96be6386e0be955ba8f65f08818b283ba6b8fba153a9996d7db26acd3b300121b0fbe8019e68a8a49c284d72de9d6a398624ca61f0160e7bdb08d778082f8fe6cd8b662943c25743fc4b2ff521690f5213d1252fc994f1ef7865368195210cdc4331a5deed52ecbf0f5cbb1ac05e87d8bd5e70e7eaeb9f0ebc7d594d1c79bde935e6966a625a6ef962c6f89d6a93f4debc135bdd2828a1e5c42c08fb5e9bde2807cc837fa2c86cf80287cec45505758d7d7031b8c6a3d2f44fb0e0f7a47a7320c3ca3f117a7fbcbec93de42854578702a03e3641474c19c9d6e5f2c1beae241d846624bcd0cc9bf065392d23153491355e3cbb39a94aac14e222593c320c337f4116e193522ec06b858693b0e74abddb61733f0c85b355a9500e2c10755b9a2868888d1e0e92bac03db91576c707da7c94dcb8632db2fb2802be63b9471118d2b695cf2d995431bf7d5fc9cd3d96ae7f83ff135d47ab86d6422ef881824dc4dcd248e2aab160b31a9a61c0a73b21a141e3fcc58235099f3fae0230625cef5e4a3629faee9e8128f95f82556a626396a662cb00886261a51032b5f47e7bec6c7f4aa790c46e65ba13137cc887c0bf3d53de82d69bdcdf61cf35415582294442c2dedabc33125078c5225e11d4aac0a4974a0f39a6e3bd7b66050f16fdc5ac69e2991b468710a9b1295173d27a52aa3304b816a590e8c8a9756241474ee374550265aca73711a0cdc8802a484cbc1bac0b678a95a156adaee01c6d5ce4cc0a166d4398d0bd9b878bcf70b038e586127708b67abb17a9e6f876986423c8543421fc882c572efd940cf803924a15847afbfc0b95b032f0a315293b2500ed02b246a057daeb3da3bd6cc90866849a216582172041075c7b0891b619f2f606297b91a45a477bf334d6998b354dd1eb6834241d43274984ccba309755f5ac55a7ee8971e046374977cea1733ece8e635cbc102ce0b3f6da5ea772be5776191f6b9dbbd1d31d02fc19fa9846d3fdda175a861fae4fb6eaa0ee2ab87ac7aeae9c0d42b9f5e8b9557ed9355491afa07e7120cb1c825e0a207955380c5ecc183b647488146f2423097611069dba72873118d127580d2dc93c0f2b821a1a5403c089f0b259b9f285aef80f8d3107ce83052055a1438e01ecc2f836368d4f3148f5687c636cec730e25b007e67349729a0e1d62e0628d42137e131651308695b7b6401d7bcc65ef7f1a3b899d4e9fc40db6ad2ab63bdc88b84e521d8075317e16736c296e6d2a7529dfa4c6fcec74f1e3b3734f5b4e7ce83cec91dd08d9ed05090e2ba5e4a2d5ebaf86f14d6cc1c455c71e3b02aade7bac9ab64c8233d6a7eb7dcc13cd2d24fbe773c42fcac4c64b99bedcca62fa62c1801ed51409a87ebd734794553cc61a0020bda32b84cb525a8b78d7a1c539c97f68512a13fc5fed0a4a880cc6534305b25838b1e7de3ada14799ca58aa91c998cf2353c44300648d42adb6d888dcf776394447437735302c3e634653441178e422007e9c86df14d9055d86114a8a879cdc9cd3686cc583993774a251280236e3a95c32af5e54779f08f118915eae1ac5960ed57aedd1297dc8ac4717293688eed3f72d64d45712ba0ddf66fc5dce4dffe51bcbd00a833ab4bfd77e17eac803f837470a18d8c942c61bf67319961d1e697ebf67d3381d5c7f2ab24f12fea2ce583f76db8e267ba9c623f11dc627bbf50ad2dd04f434d5429d354a57f96e2123f3ba60578fc55e944fbe6fe5c2a516cc4168cf81f7b0b07cea277474a172b005a0490641b5c837a4711a7324a1db8cfa4c02211259882f962cc97b2b8fcf979130ecab33f26b43b332460c12ae78f019f3037f9445ee0e7020d3a28593a944cf0d1b2bd2970e7f6bfe6cbc477b1024b639fb5bc1f4b65db41dc117d2a1b768dcd6be6eb48817912ca85cb8b0fcd66c8e3774cfd8944fd0ca97ed0bc3c636cecb6af378e98681ab89dff70d4425adc76f01dcc850deea73cca83455cf4ad12c5f95d78dd33a46f350beb71950a32c268c81d4e450c6e57debc36a58f5762df7910d82296de452c960be95f119c1e0c18001f14d29628f682150d8d8a6bae08c19c1cc3871a5e832ed2737113bbed761df09ebcc59fe2227fda12b582206cf158bfd320f2690565f6a102481804286f6d26a16df2f8a2ba9b22f64c860749c63a55a024208f7274909c0d46ab0a9c5f5497eff8f734b4d91cc80c3b4936f2fbdeb2dfa7ae66220b59c40fd35abb11ecea40ededa0e52c23e60707cda60b918d12cf1a6aa6d61dbdee847c1dfbd19e6319c8e1f2a35d7d31a622ee9bcbfc6daaf4a69de7c795fc0be6f53b72d4c4121591cf6a3ad3e7692fc65ad6990b33561ef117834e5a229b5c7a9f06c3533a887c35b6cf239421d96a0ff90af23fe2f173ee16f641dc5a6c133c84103d595c07b545190a1b4fbe703c4410a6108d17a55c247696e1deef5fb577171da3a66646c70cdc474c431f8dae0fab6a60000").unwrap(); @@ -24,8 +35,9 @@ lazy_static! { } impl MockWallet { - pub(crate) fn new() -> Self { - Self {} + pub(crate) fn new(user_signer: Arc>) -> Result { + let signer = crate::signer::SdkLwkSigner::new(user_signer.clone())?; + Ok(Self { signer }) } } @@ -35,6 +47,10 @@ impl OnchainWallet for MockWallet { Ok(vec![]) } + async fn transactions_by_tx_id(&self) -> Result, PaymentError> { + Ok(Default::default()) + } + async fn build_tx( &self, _fee_rate: Option, @@ -71,11 +87,11 @@ impl OnchainWallet for MockWallet { } fn pubkey(&self) -> Result { - unimplemented!() + Ok(self.signer.xpub()?.public_key.to_string()) } fn fingerprint(&self) -> Result { - unimplemented!() + Ok(self.signer.fingerprint()?.to_hex()) } fn sign_message(&self, _message: &str) -> Result { @@ -91,36 +107,76 @@ impl OnchainWallet for MockWallet { } } -pub(crate) struct MockSigner {} +pub(crate) struct MockSigner { + xprv: Xpriv, + secp: Secp256k1, + keypair: Keypair, +} impl MockSigner { - pub(crate) fn new() -> Self { - Self {} + pub(crate) fn new() -> Result { + let secp = Secp256k1::new(); + let mnemonic = Mnemonic::generate(12)?; + let seed = mnemonic.to_seed(""); + let xprv = Xpriv::new_master(bitcoin::Network::Testnet, &seed)?; + let keypair = xprv.to_keypair(&secp); + + Ok(Self { + xprv, + secp, + keypair, + }) } } impl Signer for MockSigner { fn xpub(&self) -> Result, SignerError> { - todo!() + Ok(Xpub::from_priv(&self.secp, &self.xprv).encode().to_vec()) } - fn derive_xpub(&self, _derivation_path: String) -> Result, SignerError> { - todo!() + fn derive_xpub(&self, derivation_path: String) -> Result, SignerError> { + let der: DerivationPath = derivation_path.parse()?; + let derived = self.xprv.derive_priv(&self.secp, &der)?; + Ok(Xpub::from_priv(&self.secp, &derived).encode().to_vec()) } fn sign_ecdsa(&self, _msg: Vec, _derivation_path: String) -> Result, SignerError> { todo!() } - fn sign_ecdsa_recoverable(&self, _msg: Vec) -> Result, SignerError> { - todo!() + fn sign_ecdsa_recoverable(&self, msg: Vec) -> Result, SignerError> { + let msg: Message = Message::from_digest_slice(msg.as_slice()) + .map_err(|e| SignerError::Generic { err: e.to_string() })?; + // Get message signature and encode to zbase32 + let recoverable_sig = self + .secp + .sign_ecdsa_recoverable(&msg, &self.keypair.secret_key()); + let (recovery_id, sig) = recoverable_sig.serialize_compact(); + let mut complete_signature = vec![31 + recovery_id.to_i32() as u8]; + complete_signature.extend_from_slice(&sig); + Ok(complete_signature) } fn slip77_master_blinding_key(&self) -> Result, SignerError> { - todo!() + let blinding_key: MasterBlindingKey = self.keypair.secret_key().secret_bytes().into(); + Ok(blinding_key.as_bytes().to_vec()) } fn hmac_sha256(&self, _msg: Vec, _derivation_path: String) -> Result, SignerError> { todo!() } + + fn ecies_encrypt(&self, msg: Vec) -> Result, SignerError> { + let rc_pub = self.keypair.public_key().to_public_key().to_bytes(); + ecies::encrypt(&rc_pub, &msg).map_err(|err| SignerError::Generic { + err: format!("Could not encrypt data: {err}"), + }) + } + + fn ecies_decrypt(&self, msg: Vec) -> Result, SignerError> { + let rc_prv = self.keypair.secret_bytes(); + ecies::decrypt(&rc_prv, &msg).map_err(|err| SignerError::Generic { + err: format!("Could not decrypt data: {err}"), + }) + } } diff --git a/lib/core/src/wallet.rs b/lib/core/src/wallet.rs index 1c91cfecb..fadec3823 100644 --- a/lib/core/src/wallet.rs +++ b/lib/core/src/wallet.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs::{self, create_dir_all}; use std::io::Write; use std::path::PathBuf; @@ -9,6 +10,7 @@ use boltz_client::ElementsAddress; use log::{debug, warn}; use lwk_common::Signer as LwkSigner; use lwk_common::{singlesig_desc, Singlesig}; +use lwk_wollet::elements::Txid; use lwk_wollet::{ elements::{hex::ToHex, Address, Transaction}, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister, Tip, WalletTx, Wollet, @@ -36,6 +38,9 @@ pub trait OnchainWallet: Send + Sync { /// List all transactions in the wallet async fn transactions(&self) -> Result, PaymentError>; + /// List all transactions in the wallet mapped by tx id + async fn transactions_by_tx_id(&self) -> Result, PaymentError>; + /// Build a transaction to send funds to a recipient async fn build_tx( &self, @@ -179,6 +184,17 @@ impl OnchainWallet for LiquidOnchainWallet { }) } + /// List all transactions in the wallet mapped by tx id + async fn transactions_by_tx_id(&self) -> Result, PaymentError> { + let tx_map: HashMap = self + .transactions() + .await? + .iter() + .map(|tx| (tx.txid, tx.clone())) + .collect(); + Ok(tx_map) + } + /// Build a transaction to send funds to a recipient async fn build_tx( &self, @@ -328,9 +344,11 @@ impl OnchainWallet for LiquidOnchainWallet { true, true, ))?; + // Use the cached derivation index with a buffer of 5 to perform the scan let index = self .persister .get_last_derivation_index()? + .map(|i| i + 5) .unwrap_or_default(); match lwk_wollet::full_scan_to_index_with_electrum_client( &mut wallet, @@ -377,12 +395,13 @@ mod tests { use super::*; use crate::model::Config; use crate::signer::SdkSigner; - use crate::test_utils::persist::new_persister; + use crate::test_utils::persist::create_persister; use crate::wallet::LiquidOnchainWallet; - use tempfile::TempDir; + use anyhow::Result; + use tempdir::TempDir; #[tokio::test] - async fn test_sign_and_check_message() { + async fn test_sign_and_check_message() -> Result<()> { let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; let sdk_signer: Box = Box::new(SdkSigner::new(mnemonic, false).unwrap()); let sdk_signer = Arc::new(sdk_signer); @@ -390,11 +409,10 @@ mod tests { let config = Config::testnet(None); // Create a temporary directory for working_dir - let temp_dir = TempDir::new().unwrap(); + let temp_dir = TempDir::new("").unwrap(); let working_dir = temp_dir.path().to_str().unwrap().to_string(); - let (_temp_dir, storage) = new_persister().unwrap(); - let storage = Arc::new(storage); + create_persister!(storage); let wallet: Arc = Arc::new( LiquidOnchainWallet::new(config, &working_dir, storage, sdk_signer.clone()).unwrap(), @@ -444,5 +462,6 @@ mod tests { ); // The temporary directory will be automatically deleted when temp_dir goes out of scope + Ok(()) } } diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index ad8b5ce89..a02364a5e 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1476,6 +1476,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_ln_url_error_data(raw); } + @protected + LnUrlInfo dco_decode_box_autoadd_ln_url_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_ln_url_info(raw); + } + @protected LnUrlPayErrorData dco_decode_box_autoadd_ln_url_pay_error_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1693,7 +1699,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { Config dco_decode_config(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 12) throw Exception('unexpected arr length: expect 12 but see ${arr.length}'); + if (arr.length != 13) throw Exception('unexpected arr length: expect 13 but see ${arr.length}'); return Config( liquidElectrumUrl: dco_decode_String(arr[0]), bitcoinElectrumUrl: dco_decode_String(arr[1]), @@ -1703,10 +1709,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: dco_decode_liquid_network(arr[5]), paymentTimeoutSec: dco_decode_u_64(arr[6]), zeroConfMinFeeRateMsat: dco_decode_u_32(arr[7]), - zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[8]), - breezApiKey: dco_decode_opt_String(arr[9]), - externalInputParsers: dco_decode_opt_list_external_input_parser(arr[10]), - useDefaultExternalInputParsers: dco_decode_bool(arr[11]), + syncServiceUrl: dco_decode_String(arr[8]), + zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[9]), + breezApiKey: dco_decode_opt_String(arr[10]), + externalInputParsers: dco_decode_opt_list_external_input_parser(arr[11]), + useDefaultExternalInputParsers: dco_decode_bool(arr[12]), ); } @@ -1957,6 +1964,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List dco_decode_list_payment_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_payment_state).toList(); + } + @protected List dco_decode_list_payment_type(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1967,14 +1980,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ListPaymentsRequest dco_decode_list_payments_request(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); return ListPaymentsRequest( filters: dco_decode_opt_list_payment_type(arr[0]), - fromTimestamp: dco_decode_opt_box_autoadd_i_64(arr[1]), - toTimestamp: dco_decode_opt_box_autoadd_i_64(arr[2]), - offset: dco_decode_opt_box_autoadd_u_32(arr[3]), - limit: dco_decode_opt_box_autoadd_u_32(arr[4]), - details: dco_decode_opt_box_autoadd_list_payment_details(arr[5]), + states: dco_decode_opt_list_payment_state(arr[1]), + fromTimestamp: dco_decode_opt_box_autoadd_i_64(arr[2]), + toTimestamp: dco_decode_opt_box_autoadd_i_64(arr[3]), + offset: dco_decode_opt_box_autoadd_u_32(arr[4]), + limit: dco_decode_opt_box_autoadd_u_32(arr[5]), + details: dco_decode_opt_box_autoadd_list_payment_details(arr[6]), ); } @@ -2115,6 +2129,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + LnUrlInfo dco_decode_ln_url_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); + return LnUrlInfo( + lnAddress: dco_decode_opt_String(arr[0]), + lnurlPayComment: dco_decode_opt_String(arr[1]), + lnurlPayDomain: dco_decode_opt_String(arr[2]), + lnurlPayMetadata: dco_decode_opt_String(arr[3]), + lnurlPaySuccessAction: dco_decode_opt_box_autoadd_success_action_processed(arr[4]), + lnurlPayUnprocessedSuccessAction: dco_decode_opt_box_autoadd_success_action(arr[5]), + lnurlWithdrawEndpoint: dco_decode_opt_String(arr[6]), + ); + } + @protected LnUrlPayError dco_decode_ln_url_pay_error(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2428,6 +2458,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_list_payment_details(raw); } + @protected + LnUrlInfo? dco_decode_opt_box_autoadd_ln_url_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_ln_url_info(raw); + } + @protected PayAmount? dco_decode_opt_box_autoadd_pay_amount(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2476,6 +2512,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_list_external_input_parser(raw); } + @protected + List? dco_decode_opt_list_payment_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_list_payment_state(raw); + } + @protected List? dco_decode_opt_list_payment_type(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2538,8 +2580,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: dco_decode_opt_String(raw[4]), bolt12Offer: dco_decode_opt_String(raw[5]), paymentHash: dco_decode_opt_String(raw[6]), - refundTxId: dco_decode_opt_String(raw[7]), - refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(raw[8]), + lnurlInfo: dco_decode_opt_box_autoadd_ln_url_info(raw[7]), + refundTxId: dco_decode_opt_String(raw[8]), + refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(raw[9]), ); case 1: return PaymentDetails_Liquid( @@ -2688,11 +2731,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { PrepareLnUrlPayResponse dco_decode_prepare_ln_url_pay_response(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + if (arr.length != 5) throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); return PrepareLnUrlPayResponse( destination: dco_decode_send_destination(arr[0]), feesSat: dco_decode_u_64(arr[1]), - successAction: dco_decode_opt_box_autoadd_success_action(arr[2]), + data: dco_decode_ln_url_pay_request_data(arr[2]), + comment: dco_decode_opt_String(arr[3]), + successAction: dco_decode_opt_box_autoadd_success_action(arr[4]), ); } @@ -3397,6 +3442,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_ln_url_error_data(deserializer)); } + @protected + LnUrlInfo sse_decode_box_autoadd_ln_url_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_ln_url_info(deserializer)); + } + @protected LnUrlPayErrorData sse_decode_box_autoadd_ln_url_pay_error_data(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3613,6 +3664,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_network = sse_decode_liquid_network(deserializer); var var_paymentTimeoutSec = sse_decode_u_64(deserializer); var var_zeroConfMinFeeRateMsat = sse_decode_u_32(deserializer); + var var_syncServiceUrl = sse_decode_String(deserializer); var var_zeroConfMaxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_breezApiKey = sse_decode_opt_String(deserializer); var var_externalInputParsers = sse_decode_opt_list_external_input_parser(deserializer); @@ -3626,6 +3678,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: var_network, paymentTimeoutSec: var_paymentTimeoutSec, zeroConfMinFeeRateMsat: var_zeroConfMinFeeRateMsat, + syncServiceUrl: var_syncServiceUrl, zeroConfMaxAmountSat: var_zeroConfMaxAmountSat, breezApiKey: var_breezApiKey, externalInputParsers: var_externalInputParsers, @@ -3910,6 +3963,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List sse_decode_list_payment_state(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_payment_state(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_payment_type(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3926,6 +3991,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ListPaymentsRequest sse_decode_list_payments_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_filters = sse_decode_opt_list_payment_type(deserializer); + var var_states = sse_decode_opt_list_payment_state(deserializer); var var_fromTimestamp = sse_decode_opt_box_autoadd_i_64(deserializer); var var_toTimestamp = sse_decode_opt_box_autoadd_i_64(deserializer); var var_offset = sse_decode_opt_box_autoadd_u_32(deserializer); @@ -3933,6 +3999,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_details = sse_decode_opt_box_autoadd_list_payment_details(deserializer); return ListPaymentsRequest( filters: var_filters, + states: var_states, fromTimestamp: var_fromTimestamp, toTimestamp: var_toTimestamp, offset: var_offset, @@ -4107,6 +4174,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return LnUrlErrorData(reason: var_reason); } + @protected + LnUrlInfo sse_decode_ln_url_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_lnAddress = sse_decode_opt_String(deserializer); + var var_lnurlPayComment = sse_decode_opt_String(deserializer); + var var_lnurlPayDomain = sse_decode_opt_String(deserializer); + var var_lnurlPayMetadata = sse_decode_opt_String(deserializer); + var var_lnurlPaySuccessAction = sse_decode_opt_box_autoadd_success_action_processed(deserializer); + var var_lnurlPayUnprocessedSuccessAction = sse_decode_opt_box_autoadd_success_action(deserializer); + var var_lnurlWithdrawEndpoint = sse_decode_opt_String(deserializer); + return LnUrlInfo( + lnAddress: var_lnAddress, + lnurlPayComment: var_lnurlPayComment, + lnurlPayDomain: var_lnurlPayDomain, + lnurlPayMetadata: var_lnurlPayMetadata, + lnurlPaySuccessAction: var_lnurlPaySuccessAction, + lnurlPayUnprocessedSuccessAction: var_lnurlPayUnprocessedSuccessAction, + lnurlWithdrawEndpoint: var_lnurlWithdrawEndpoint); + } + @protected LnUrlPayError sse_decode_ln_url_pay_error(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4414,6 +4501,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + LnUrlInfo? sse_decode_opt_box_autoadd_ln_url_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_ln_url_info(deserializer)); + } else { + return null; + } + } + @protected PayAmount? sse_decode_opt_box_autoadd_pay_amount(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4502,6 +4600,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List? sse_decode_opt_list_payment_state(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_payment_state(deserializer)); + } else { + return null; + } + } + @protected List? sse_decode_opt_list_payment_type(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4574,6 +4683,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_bolt11 = sse_decode_opt_String(deserializer); var var_bolt12Offer = sse_decode_opt_String(deserializer); var var_paymentHash = sse_decode_opt_String(deserializer); + var var_lnurlInfo = sse_decode_opt_box_autoadd_ln_url_info(deserializer); var var_refundTxId = sse_decode_opt_String(deserializer); var var_refundTxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); return PaymentDetails_Lightning( @@ -4583,6 +4693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: var_bolt11, bolt12Offer: var_bolt12Offer, paymentHash: var_paymentHash, + lnurlInfo: var_lnurlInfo, refundTxId: var_refundTxId, refundTxAmountSat: var_refundTxAmountSat); case 1: @@ -4725,9 +4836,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs var var_destination = sse_decode_send_destination(deserializer); var var_feesSat = sse_decode_u_64(deserializer); + var var_data = sse_decode_ln_url_pay_request_data(deserializer); + var var_comment = sse_decode_opt_String(deserializer); var var_successAction = sse_decode_opt_box_autoadd_success_action(deserializer); return PrepareLnUrlPayResponse( - destination: var_destination, feesSat: var_feesSat, successAction: var_successAction); + destination: var_destination, + feesSat: var_feesSat, + data: var_data, + comment: var_comment, + successAction: var_successAction); } @protected @@ -5476,6 +5593,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_ln_url_error_data(self, serializer); } + @protected + void sse_encode_box_autoadd_ln_url_info(LnUrlInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_ln_url_info(self, serializer); + } + @protected void sse_encode_box_autoadd_ln_url_pay_error_data(LnUrlPayErrorData self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5695,6 +5818,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_liquid_network(self.network, serializer); sse_encode_u_64(self.paymentTimeoutSec, serializer); sse_encode_u_32(self.zeroConfMinFeeRateMsat, serializer); + sse_encode_String(self.syncServiceUrl, serializer); sse_encode_opt_box_autoadd_u_64(self.zeroConfMaxAmountSat, serializer); sse_encode_opt_String(self.breezApiKey, serializer); sse_encode_opt_list_external_input_parser(self.externalInputParsers, serializer); @@ -5925,6 +6049,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_payment_state(List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_payment_state(item, serializer); + } + } + @protected void sse_encode_list_payment_type(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5938,6 +6071,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { void sse_encode_list_payments_request(ListPaymentsRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_opt_list_payment_type(self.filters, serializer); + sse_encode_opt_list_payment_state(self.states, serializer); sse_encode_opt_box_autoadd_i_64(self.fromTimestamp, serializer); sse_encode_opt_box_autoadd_i_64(self.toTimestamp, serializer); sse_encode_opt_box_autoadd_u_32(self.offset, serializer); @@ -6071,6 +6205,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.reason, serializer); } + @protected + void sse_encode_ln_url_info(LnUrlInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_String(self.lnAddress, serializer); + sse_encode_opt_String(self.lnurlPayComment, serializer); + sse_encode_opt_String(self.lnurlPayDomain, serializer); + sse_encode_opt_String(self.lnurlPayMetadata, serializer); + sse_encode_opt_box_autoadd_success_action_processed(self.lnurlPaySuccessAction, serializer); + sse_encode_opt_box_autoadd_success_action(self.lnurlPayUnprocessedSuccessAction, serializer); + sse_encode_opt_String(self.lnurlWithdrawEndpoint, serializer); + } + @protected void sse_encode_ln_url_pay_error(LnUrlPayError self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6338,6 +6484,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_ln_url_info(LnUrlInfo? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_ln_url_info(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_pay_amount(PayAmount? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6419,6 +6575,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_list_payment_state(List? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_list_payment_state(self, serializer); + } + } + @protected void sse_encode_opt_list_payment_type(List? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6475,6 +6641,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: final bolt11, bolt12Offer: final bolt12Offer, paymentHash: final paymentHash, + lnurlInfo: final lnurlInfo, refundTxId: final refundTxId, refundTxAmountSat: final refundTxAmountSat ): @@ -6485,6 +6652,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(bolt11, serializer); sse_encode_opt_String(bolt12Offer, serializer); sse_encode_opt_String(paymentHash, serializer); + sse_encode_opt_box_autoadd_ln_url_info(lnurlInfo, serializer); sse_encode_opt_String(refundTxId, serializer); sse_encode_opt_box_autoadd_u_64(refundTxAmountSat, serializer); case PaymentDetails_Liquid(destination: final destination, description: final description): @@ -6616,6 +6784,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_send_destination(self.destination, serializer); sse_encode_u_64(self.feesSat, serializer); + sse_encode_ln_url_pay_request_data(self.data, serializer); + sse_encode_opt_String(self.comment, serializer); sse_encode_opt_box_autoadd_success_action(self.successAction, serializer); } diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index 1cf59d54e..7bbf04c83 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -137,6 +137,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LnUrlErrorData dco_decode_box_autoadd_ln_url_error_data(dynamic raw); + @protected + LnUrlInfo dco_decode_box_autoadd_ln_url_info(dynamic raw); + @protected LnUrlPayErrorData dco_decode_box_autoadd_ln_url_pay_error_data(dynamic raw); @@ -305,6 +308,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected ListPaymentDetails dco_decode_list_payment_details(dynamic raw); + @protected + List dco_decode_list_payment_state(dynamic raw); + @protected List dco_decode_list_payment_type(dynamic raw); @@ -347,6 +353,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LnUrlErrorData dco_decode_ln_url_error_data(dynamic raw); + @protected + LnUrlInfo dco_decode_ln_url_info(dynamic raw); + @protected LnUrlPayError dco_decode_ln_url_pay_error(dynamic raw); @@ -416,6 +425,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected ListPaymentDetails? dco_decode_opt_box_autoadd_list_payment_details(dynamic raw); + @protected + LnUrlInfo? dco_decode_opt_box_autoadd_ln_url_info(dynamic raw); + @protected PayAmount? dco_decode_opt_box_autoadd_pay_amount(dynamic raw); @@ -440,6 +452,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List? dco_decode_opt_list_external_input_parser(dynamic raw); + @protected + List? dco_decode_opt_list_payment_state(dynamic raw); + @protected List? dco_decode_opt_list_payment_type(dynamic raw); @@ -699,6 +714,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LnUrlErrorData sse_decode_box_autoadd_ln_url_error_data(SseDeserializer deserializer); + @protected + LnUrlInfo sse_decode_box_autoadd_ln_url_info(SseDeserializer deserializer); + @protected LnUrlPayErrorData sse_decode_box_autoadd_ln_url_pay_error_data(SseDeserializer deserializer); @@ -867,6 +885,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected ListPaymentDetails sse_decode_list_payment_details(SseDeserializer deserializer); + @protected + List sse_decode_list_payment_state(SseDeserializer deserializer); + @protected List sse_decode_list_payment_type(SseDeserializer deserializer); @@ -909,6 +930,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected LnUrlErrorData sse_decode_ln_url_error_data(SseDeserializer deserializer); + @protected + LnUrlInfo sse_decode_ln_url_info(SseDeserializer deserializer); + @protected LnUrlPayError sse_decode_ln_url_pay_error(SseDeserializer deserializer); @@ -978,6 +1002,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected ListPaymentDetails? sse_decode_opt_box_autoadd_list_payment_details(SseDeserializer deserializer); + @protected + LnUrlInfo? sse_decode_opt_box_autoadd_ln_url_info(SseDeserializer deserializer); + @protected PayAmount? sse_decode_opt_box_autoadd_pay_amount(SseDeserializer deserializer); @@ -1002,6 +1029,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List? sse_decode_opt_list_external_input_parser(SseDeserializer deserializer); + @protected + List? sse_decode_opt_list_payment_state(SseDeserializer deserializer); + @protected List? sse_decode_opt_list_payment_type(SseDeserializer deserializer); @@ -1354,6 +1384,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return ptr; } + @protected + ffi.Pointer cst_encode_box_autoadd_ln_url_info(LnUrlInfo raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + final ptr = wire.cst_new_box_autoadd_ln_url_info(); + cst_api_fill_to_wire_ln_url_info(raw, ptr.ref); + return ptr; + } + @protected ffi.Pointer cst_encode_box_autoadd_ln_url_pay_error_data( LnUrlPayErrorData raw) { @@ -1679,6 +1717,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return ans; } + @protected + ffi.Pointer cst_encode_list_payment_state(List raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + final ans = wire.cst_new_list_payment_state(raw.length); + for (var i = 0; i < raw.length; ++i) { + ans.ref.ptr[i] = cst_encode_payment_state(raw[i]); + } + return ans; + } + @protected ffi.Pointer cst_encode_list_payment_type(List raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -1774,6 +1822,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return raw == null ? ffi.nullptr : cst_encode_box_autoadd_list_payment_details(raw); } + @protected + ffi.Pointer cst_encode_opt_box_autoadd_ln_url_info(LnUrlInfo? raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw == null ? ffi.nullptr : cst_encode_box_autoadd_ln_url_info(raw); + } + @protected ffi.Pointer cst_encode_opt_box_autoadd_pay_amount(PayAmount? raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -1824,6 +1878,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return raw == null ? ffi.nullptr : cst_encode_list_external_input_parser(raw); } + @protected + ffi.Pointer cst_encode_opt_list_payment_state(List? raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw == null ? ffi.nullptr : cst_encode_list_payment_state(raw); + } + @protected ffi.Pointer cst_encode_opt_list_payment_type(List? raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -2019,6 +2079,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { cst_api_fill_to_wire_ln_url_error_data(apiObj, wireObj.ref); } + @protected + void cst_api_fill_to_wire_box_autoadd_ln_url_info( + LnUrlInfo apiObj, ffi.Pointer wireObj) { + cst_api_fill_to_wire_ln_url_info(apiObj, wireObj.ref); + } + @protected void cst_api_fill_to_wire_box_autoadd_ln_url_pay_error_data( LnUrlPayErrorData apiObj, ffi.Pointer wireObj) { @@ -2209,6 +2275,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.network = cst_encode_liquid_network(apiObj.network); wireObj.payment_timeout_sec = cst_encode_u_64(apiObj.paymentTimeoutSec); wireObj.zero_conf_min_fee_rate_msat = cst_encode_u_32(apiObj.zeroConfMinFeeRateMsat); + wireObj.sync_service_url = cst_encode_String(apiObj.syncServiceUrl); wireObj.zero_conf_max_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.zeroConfMaxAmountSat); wireObj.breez_api_key = cst_encode_opt_String(apiObj.breezApiKey); wireObj.external_input_parsers = cst_encode_opt_list_external_input_parser(apiObj.externalInputParsers); @@ -2376,6 +2443,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void cst_api_fill_to_wire_list_payments_request( ListPaymentsRequest apiObj, wire_cst_list_payments_request wireObj) { wireObj.filters = cst_encode_opt_list_payment_type(apiObj.filters); + wireObj.states = cst_encode_opt_list_payment_state(apiObj.states); wireObj.from_timestamp = cst_encode_opt_box_autoadd_i_64(apiObj.fromTimestamp); wireObj.to_timestamp = cst_encode_opt_box_autoadd_i_64(apiObj.toTimestamp); wireObj.offset = cst_encode_opt_box_autoadd_u_32(apiObj.offset); @@ -2468,6 +2536,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.reason = cst_encode_String(apiObj.reason); } + @protected + void cst_api_fill_to_wire_ln_url_info(LnUrlInfo apiObj, wire_cst_ln_url_info wireObj) { + wireObj.ln_address = cst_encode_opt_String(apiObj.lnAddress); + wireObj.lnurl_pay_comment = cst_encode_opt_String(apiObj.lnurlPayComment); + wireObj.lnurl_pay_domain = cst_encode_opt_String(apiObj.lnurlPayDomain); + wireObj.lnurl_pay_metadata = cst_encode_opt_String(apiObj.lnurlPayMetadata); + wireObj.lnurl_pay_success_action = + cst_encode_opt_box_autoadd_success_action_processed(apiObj.lnurlPaySuccessAction); + wireObj.lnurl_pay_unprocessed_success_action = + cst_encode_opt_box_autoadd_success_action(apiObj.lnurlPayUnprocessedSuccessAction); + wireObj.lnurl_withdraw_endpoint = cst_encode_opt_String(apiObj.lnurlWithdrawEndpoint); + } + @protected void cst_api_fill_to_wire_ln_url_pay_error(LnUrlPayError apiObj, wire_cst_ln_url_pay_error wireObj) { if (apiObj is LnUrlPayError_AlreadyPaid) { @@ -2760,6 +2841,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { var pre_bolt11 = cst_encode_opt_String(apiObj.bolt11); var pre_bolt12_offer = cst_encode_opt_String(apiObj.bolt12Offer); var pre_payment_hash = cst_encode_opt_String(apiObj.paymentHash); + var pre_lnurl_info = cst_encode_opt_box_autoadd_ln_url_info(apiObj.lnurlInfo); var pre_refund_tx_id = cst_encode_opt_String(apiObj.refundTxId); var pre_refund_tx_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.refundTxAmountSat); wireObj.tag = 0; @@ -2769,6 +2851,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.kind.Lightning.bolt11 = pre_bolt11; wireObj.kind.Lightning.bolt12_offer = pre_bolt12_offer; wireObj.kind.Lightning.payment_hash = pre_payment_hash; + wireObj.kind.Lightning.lnurl_info = pre_lnurl_info; wireObj.kind.Lightning.refund_tx_id = pre_refund_tx_id; wireObj.kind.Lightning.refund_tx_amount_sat = pre_refund_tx_amount_sat; return; @@ -2934,6 +3017,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { PrepareLnUrlPayResponse apiObj, wire_cst_prepare_ln_url_pay_response wireObj) { cst_api_fill_to_wire_send_destination(apiObj.destination, wireObj.destination); wireObj.fees_sat = cst_encode_u_64(apiObj.feesSat); + cst_api_fill_to_wire_ln_url_pay_request_data(apiObj.data, wireObj.data); + wireObj.comment = cst_encode_opt_String(apiObj.comment); wireObj.success_action = cst_encode_opt_box_autoadd_success_action(apiObj.successAction); } @@ -3411,6 +3496,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_ln_url_error_data(LnUrlErrorData self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_ln_url_info(LnUrlInfo self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_ln_url_pay_error_data(LnUrlPayErrorData self, SseSerializer serializer); @@ -3586,6 +3674,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_list_payment_details(ListPaymentDetails self, SseSerializer serializer); + @protected + void sse_encode_list_payment_state(List self, SseSerializer serializer); + @protected void sse_encode_list_payment_type(List self, SseSerializer serializer); @@ -3628,6 +3719,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_ln_url_error_data(LnUrlErrorData self, SseSerializer serializer); + @protected + void sse_encode_ln_url_info(LnUrlInfo self, SseSerializer serializer); + @protected void sse_encode_ln_url_pay_error(LnUrlPayError self, SseSerializer serializer); @@ -3698,6 +3792,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_list_payment_details(ListPaymentDetails? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_ln_url_info(LnUrlInfo? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_pay_amount(PayAmount? self, SseSerializer serializer); @@ -3723,6 +3820,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_list_external_input_parser(List? self, SseSerializer serializer); + @protected + void sse_encode_opt_list_payment_state(List? self, SseSerializer serializer); + @protected void sse_encode_opt_list_payment_type(List? self, SseSerializer serializer); @@ -4889,6 +4989,16 @@ class RustLibWire implements BaseWire { late final _cst_new_box_autoadd_ln_url_error_data = _cst_new_box_autoadd_ln_url_error_dataPtr .asFunction Function()>(); + ffi.Pointer cst_new_box_autoadd_ln_url_info() { + return _cst_new_box_autoadd_ln_url_info(); + } + + late final _cst_new_box_autoadd_ln_url_infoPtr = + _lookup Function()>>( + 'frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info'); + late final _cst_new_box_autoadd_ln_url_info = + _cst_new_box_autoadd_ln_url_infoPtr.asFunction Function()>(); + ffi.Pointer cst_new_box_autoadd_ln_url_pay_error_data() { return _cst_new_box_autoadd_ln_url_pay_error_data(); } @@ -5291,6 +5401,20 @@ class RustLibWire implements BaseWire { late final _cst_new_list_payment = _cst_new_list_paymentPtr.asFunction Function(int)>(); + ffi.Pointer cst_new_list_payment_state( + int len, + ) { + return _cst_new_list_payment_state( + len, + ); + } + + late final _cst_new_list_payment_statePtr = + _lookup Function(ffi.Int32)>>( + 'frbgen_breez_liquid_cst_new_list_payment_state'); + late final _cst_new_list_payment_state = + _cst_new_list_payment_statePtr.asFunction Function(int)>(); + ffi.Pointer cst_new_list_payment_type( int len, ) { @@ -5450,6 +5574,13 @@ final class wire_cst_list_payment_type extends ffi.Struct { external int len; } +final class wire_cst_list_payment_state extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_ListPaymentDetails_Liquid extends ffi.Struct { external ffi.Pointer destination; } @@ -5474,6 +5605,8 @@ final class wire_cst_list_payment_details extends ffi.Struct { final class wire_cst_list_payments_request extends ffi.Struct { external ffi.Pointer filters; + external ffi.Pointer states; + external ffi.Pointer from_timestamp; external ffi.Pointer to_timestamp; @@ -5668,6 +5801,30 @@ final class wire_cst_send_destination extends ffi.Struct { external SendDestinationKind kind; } +final class wire_cst_ln_url_pay_request_data extends ffi.Struct { + external ffi.Pointer callback; + + @ffi.Uint64() + external int min_sendable; + + @ffi.Uint64() + external int max_sendable; + + external ffi.Pointer metadata_str; + + @ffi.Uint16() + external int comment_allowed; + + external ffi.Pointer domain; + + @ffi.Bool() + external bool allows_nostr; + + external ffi.Pointer nostr_pubkey; + + external ffi.Pointer ln_address; +} + final class wire_cst_aes_success_action_data extends ffi.Struct { external ffi.Pointer description; @@ -5722,6 +5879,10 @@ final class wire_cst_prepare_ln_url_pay_response extends ffi.Struct { @ffi.Uint64() external int fees_sat; + external wire_cst_ln_url_pay_request_data data; + + external ffi.Pointer comment; + external ffi.Pointer success_action; } @@ -5777,30 +5938,6 @@ final class wire_cst_prepare_buy_bitcoin_request extends ffi.Struct { external int amount_sat; } -final class wire_cst_ln_url_pay_request_data extends ffi.Struct { - external ffi.Pointer callback; - - @ffi.Uint64() - external int min_sendable; - - @ffi.Uint64() - external int max_sendable; - - external ffi.Pointer metadata_str; - - @ffi.Uint16() - external int comment_allowed; - - external ffi.Pointer domain; - - @ffi.Bool() - external bool allows_nostr; - - external ffi.Pointer nostr_pubkey; - - external ffi.Pointer ln_address; -} - final class wire_cst_prepare_ln_url_pay_request extends ffi.Struct { external wire_cst_ln_url_pay_request_data data; @@ -5912,6 +6049,76 @@ final class wire_cst_binding_event_listener extends ffi.Struct { external ffi.Pointer stream; } +final class wire_cst_aes_success_action_data_decrypted extends ffi.Struct { + external ffi.Pointer description; + + external ffi.Pointer plaintext; +} + +final class wire_cst_AesSuccessActionDataResult_Decrypted extends ffi.Struct { + external ffi.Pointer data; +} + +final class wire_cst_AesSuccessActionDataResult_ErrorStatus extends ffi.Struct { + external ffi.Pointer reason; +} + +final class AesSuccessActionDataResultKind extends ffi.Union { + external wire_cst_AesSuccessActionDataResult_Decrypted Decrypted; + + external wire_cst_AesSuccessActionDataResult_ErrorStatus ErrorStatus; +} + +final class wire_cst_aes_success_action_data_result extends ffi.Struct { + @ffi.Int32() + external int tag; + + external AesSuccessActionDataResultKind kind; +} + +final class wire_cst_SuccessActionProcessed_Aes extends ffi.Struct { + external ffi.Pointer result; +} + +final class wire_cst_SuccessActionProcessed_Message extends ffi.Struct { + external ffi.Pointer data; +} + +final class wire_cst_SuccessActionProcessed_Url extends ffi.Struct { + external ffi.Pointer data; +} + +final class SuccessActionProcessedKind extends ffi.Union { + external wire_cst_SuccessActionProcessed_Aes Aes; + + external wire_cst_SuccessActionProcessed_Message Message; + + external wire_cst_SuccessActionProcessed_Url Url; +} + +final class wire_cst_success_action_processed extends ffi.Struct { + @ffi.Int32() + external int tag; + + external SuccessActionProcessedKind kind; +} + +final class wire_cst_ln_url_info extends ffi.Struct { + external ffi.Pointer ln_address; + + external ffi.Pointer lnurl_pay_comment; + + external ffi.Pointer lnurl_pay_domain; + + external ffi.Pointer lnurl_pay_metadata; + + external ffi.Pointer lnurl_pay_success_action; + + external ffi.Pointer lnurl_pay_unprocessed_success_action; + + external ffi.Pointer lnurl_withdraw_endpoint; +} + final class wire_cst_PaymentDetails_Lightning extends ffi.Struct { external ffi.Pointer swap_id; @@ -5925,6 +6132,8 @@ final class wire_cst_PaymentDetails_Lightning extends ffi.Struct { external ffi.Pointer payment_hash; + external ffi.Pointer lnurl_info; + external ffi.Pointer refund_tx_id; external ffi.Pointer refund_tx_amount_sat; @@ -6066,6 +6275,8 @@ final class wire_cst_config extends ffi.Struct { @ffi.Uint32() external int zero_conf_min_fee_rate_msat; + external ffi.Pointer sync_service_url; + external ffi.Pointer zero_conf_max_amount_sat; external ffi.Pointer breez_api_key; @@ -6082,33 +6293,6 @@ final class wire_cst_connect_request extends ffi.Struct { external ffi.Pointer mnemonic; } -final class wire_cst_aes_success_action_data_decrypted extends ffi.Struct { - external ffi.Pointer description; - - external ffi.Pointer plaintext; -} - -final class wire_cst_AesSuccessActionDataResult_Decrypted extends ffi.Struct { - external ffi.Pointer data; -} - -final class wire_cst_AesSuccessActionDataResult_ErrorStatus extends ffi.Struct { - external ffi.Pointer reason; -} - -final class AesSuccessActionDataResultKind extends ffi.Union { - external wire_cst_AesSuccessActionDataResult_Decrypted Decrypted; - - external wire_cst_AesSuccessActionDataResult_ErrorStatus ErrorStatus; -} - -final class wire_cst_aes_success_action_data_result extends ffi.Struct { - @ffi.Int32() - external int tag; - - external AesSuccessActionDataResultKind kind; -} - final class wire_cst_bitcoin_address_data extends ffi.Struct { external ffi.Pointer address; @@ -6132,33 +6316,6 @@ final class wire_cst_ln_url_pay_error_data extends ffi.Struct { external ffi.Pointer reason; } -final class wire_cst_SuccessActionProcessed_Aes extends ffi.Struct { - external ffi.Pointer result; -} - -final class wire_cst_SuccessActionProcessed_Message extends ffi.Struct { - external ffi.Pointer data; -} - -final class wire_cst_SuccessActionProcessed_Url extends ffi.Struct { - external ffi.Pointer data; -} - -final class SuccessActionProcessedKind extends ffi.Union { - external wire_cst_SuccessActionProcessed_Aes Aes; - - external wire_cst_SuccessActionProcessed_Message Message; - - external wire_cst_SuccessActionProcessed_Url Url; -} - -final class wire_cst_success_action_processed extends ffi.Struct { - @ffi.Int32() - external int tag; - - external SuccessActionProcessedKind kind; -} - final class wire_cst_ln_url_pay_success_data extends ffi.Struct { external wire_cst_payment payment; diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index 8279da70c..1e5b7924f 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -134,6 +134,9 @@ class Config { /// Zero-conf minimum accepted fee-rate in millisatoshis per vbyte final int zeroConfMinFeeRateMsat; + /// The url of the real-time sync service + final String syncServiceUrl; + /// Maximum amount in satoshi to accept zero-conf payments with /// Defaults to [DEFAULT_ZERO_CONF_MAX_SAT] final BigInt? zeroConfMaxAmountSat; @@ -160,6 +163,7 @@ class Config { required this.network, required this.paymentTimeoutSec, required this.zeroConfMinFeeRateMsat, + required this.syncServiceUrl, this.zeroConfMaxAmountSat, this.breezApiKey, this.externalInputParsers, @@ -176,6 +180,7 @@ class Config { network.hashCode ^ paymentTimeoutSec.hashCode ^ zeroConfMinFeeRateMsat.hashCode ^ + syncServiceUrl.hashCode ^ zeroConfMaxAmountSat.hashCode ^ breezApiKey.hashCode ^ externalInputParsers.hashCode ^ @@ -194,6 +199,7 @@ class Config { network == other.network && paymentTimeoutSec == other.paymentTimeoutSec && zeroConfMinFeeRateMsat == other.zeroConfMinFeeRateMsat && + syncServiceUrl == other.syncServiceUrl && zeroConfMaxAmountSat == other.zeroConfMaxAmountSat && breezApiKey == other.breezApiKey && externalInputParsers == other.externalInputParsers && @@ -356,6 +362,7 @@ sealed class ListPaymentDetails with _$ListPaymentDetails { /// An argument when calling [crate::sdk::LiquidSdk::list_payments]. class ListPaymentsRequest { final List? filters; + final List? states; /// Epoch time, in seconds final PlatformInt64? fromTimestamp; @@ -368,6 +375,7 @@ class ListPaymentsRequest { const ListPaymentsRequest({ this.filters, + this.states, this.fromTimestamp, this.toTimestamp, this.offset, @@ -378,6 +386,7 @@ class ListPaymentsRequest { @override int get hashCode => filters.hashCode ^ + states.hashCode ^ fromTimestamp.hashCode ^ toTimestamp.hashCode ^ offset.hashCode ^ @@ -390,6 +399,7 @@ class ListPaymentsRequest { other is ListPaymentsRequest && runtimeType == other.runtimeType && filters == other.filters && + states == other.states && fromTimestamp == other.fromTimestamp && toTimestamp == other.toTimestamp && offset == other.offset && @@ -397,6 +407,50 @@ class ListPaymentsRequest { details == other.details; } +/// Represents the payment LNURL info +class LnUrlInfo { + final String? lnAddress; + final String? lnurlPayComment; + final String? lnurlPayDomain; + final String? lnurlPayMetadata; + final SuccessActionProcessed? lnurlPaySuccessAction; + final SuccessAction? lnurlPayUnprocessedSuccessAction; + final String? lnurlWithdrawEndpoint; + + const LnUrlInfo({ + this.lnAddress, + this.lnurlPayComment, + this.lnurlPayDomain, + this.lnurlPayMetadata, + this.lnurlPaySuccessAction, + this.lnurlPayUnprocessedSuccessAction, + this.lnurlWithdrawEndpoint, + }); + + @override + int get hashCode => + lnAddress.hashCode ^ + lnurlPayComment.hashCode ^ + lnurlPayDomain.hashCode ^ + lnurlPayMetadata.hashCode ^ + lnurlPaySuccessAction.hashCode ^ + lnurlPayUnprocessedSuccessAction.hashCode ^ + lnurlWithdrawEndpoint.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LnUrlInfo && + runtimeType == other.runtimeType && + lnAddress == other.lnAddress && + lnurlPayComment == other.lnurlPayComment && + lnurlPayDomain == other.lnurlPayDomain && + lnurlPayMetadata == other.lnurlPayMetadata && + lnurlPaySuccessAction == other.lnurlPaySuccessAction && + lnurlPayUnprocessedSuccessAction == other.lnurlPayUnprocessedSuccessAction && + lnurlWithdrawEndpoint == other.lnurlWithdrawEndpoint; +} + /// An argument when calling [crate::sdk::LiquidSdk::lnurl_pay]. class LnUrlPayRequest { /// The response from calling [crate::sdk::LiquidSdk::prepare_lnurl_pay] @@ -649,6 +703,9 @@ sealed class PaymentDetails with _$PaymentDetails { /// The payment hash of the invoice String? paymentHash, + /// The payment LNURL info + LnUrlInfo? lnurlInfo, + /// For a Send swap which was refunded, this is the refund tx id String? refundTxId, @@ -854,6 +911,12 @@ class PrepareLnUrlPayResponse { /// The fees in satoshis to send the payment final BigInt feesSat; + /// The [LnUrlPayRequestData] returned by [crate::input_parser::parse] + final LnUrlPayRequestData data; + + /// An optional comment for this payment + final String? comment; + /// The unprocessed LUD-09 success action. This will be processed and decrypted if /// needed after calling [crate::sdk::LiquidSdk::lnurl_pay] final SuccessAction? successAction; @@ -861,11 +924,14 @@ class PrepareLnUrlPayResponse { const PrepareLnUrlPayResponse({ required this.destination, required this.feesSat, + required this.data, + this.comment, this.successAction, }); @override - int get hashCode => destination.hashCode ^ feesSat.hashCode ^ successAction.hashCode; + int get hashCode => + destination.hashCode ^ feesSat.hashCode ^ data.hashCode ^ comment.hashCode ^ successAction.hashCode; @override bool operator ==(Object other) => @@ -874,6 +940,8 @@ class PrepareLnUrlPayResponse { runtimeType == other.runtimeType && destination == other.destination && feesSat == other.feesSat && + data == other.data && + comment == other.comment && successAction == other.successAction; } diff --git a/packages/dart/lib/src/model.freezed.dart b/packages/dart/lib/src/model.freezed.dart index c511a99f3..a712d5559 100644 --- a/packages/dart/lib/src/model.freezed.dart +++ b/packages/dart/lib/src/model.freezed.dart @@ -795,6 +795,7 @@ abstract class _$$PaymentDetails_LightningImplCopyWith<$Res> implements $Payment String? bolt11, String? bolt12Offer, String? paymentHash, + LnUrlInfo? lnurlInfo, String? refundTxId, BigInt? refundTxAmountSat}); } @@ -818,6 +819,7 @@ class __$$PaymentDetails_LightningImplCopyWithImpl<$Res> Object? bolt11 = freezed, Object? bolt12Offer = freezed, Object? paymentHash = freezed, + Object? lnurlInfo = freezed, Object? refundTxId = freezed, Object? refundTxAmountSat = freezed, }) { @@ -846,6 +848,10 @@ class __$$PaymentDetails_LightningImplCopyWithImpl<$Res> ? _value.paymentHash : paymentHash // ignore: cast_nullable_to_non_nullable as String?, + lnurlInfo: freezed == lnurlInfo + ? _value.lnurlInfo + : lnurlInfo // ignore: cast_nullable_to_non_nullable + as LnUrlInfo?, refundTxId: freezed == refundTxId ? _value.refundTxId : refundTxId // ignore: cast_nullable_to_non_nullable @@ -868,6 +874,7 @@ class _$PaymentDetails_LightningImpl extends PaymentDetails_Lightning { this.bolt11, this.bolt12Offer, this.paymentHash, + this.lnurlInfo, this.refundTxId, this.refundTxAmountSat}) : super._(); @@ -895,6 +902,10 @@ class _$PaymentDetails_LightningImpl extends PaymentDetails_Lightning { @override final String? paymentHash; + /// The payment LNURL info + @override + final LnUrlInfo? lnurlInfo; + /// For a Send swap which was refunded, this is the refund tx id @override final String? refundTxId; @@ -905,7 +916,7 @@ class _$PaymentDetails_LightningImpl extends PaymentDetails_Lightning { @override String toString() { - return 'PaymentDetails.lightning(swapId: $swapId, description: $description, preimage: $preimage, bolt11: $bolt11, bolt12Offer: $bolt12Offer, paymentHash: $paymentHash, refundTxId: $refundTxId, refundTxAmountSat: $refundTxAmountSat)'; + return 'PaymentDetails.lightning(swapId: $swapId, description: $description, preimage: $preimage, bolt11: $bolt11, bolt12Offer: $bolt12Offer, paymentHash: $paymentHash, lnurlInfo: $lnurlInfo, refundTxId: $refundTxId, refundTxAmountSat: $refundTxAmountSat)'; } @override @@ -919,6 +930,7 @@ class _$PaymentDetails_LightningImpl extends PaymentDetails_Lightning { (identical(other.bolt11, bolt11) || other.bolt11 == bolt11) && (identical(other.bolt12Offer, bolt12Offer) || other.bolt12Offer == bolt12Offer) && (identical(other.paymentHash, paymentHash) || other.paymentHash == paymentHash) && + (identical(other.lnurlInfo, lnurlInfo) || other.lnurlInfo == lnurlInfo) && (identical(other.refundTxId, refundTxId) || other.refundTxId == refundTxId) && (identical(other.refundTxAmountSat, refundTxAmountSat) || other.refundTxAmountSat == refundTxAmountSat)); @@ -926,7 +938,7 @@ class _$PaymentDetails_LightningImpl extends PaymentDetails_Lightning { @override int get hashCode => Object.hash(runtimeType, swapId, description, preimage, bolt11, bolt12Offer, - paymentHash, refundTxId, refundTxAmountSat); + paymentHash, lnurlInfo, refundTxId, refundTxAmountSat); /// Create a copy of PaymentDetails /// with the given fields replaced by the non-null parameter values. @@ -945,6 +957,7 @@ abstract class PaymentDetails_Lightning extends PaymentDetails { final String? bolt11, final String? bolt12Offer, final String? paymentHash, + final LnUrlInfo? lnurlInfo, final String? refundTxId, final BigInt? refundTxAmountSat}) = _$PaymentDetails_LightningImpl; const PaymentDetails_Lightning._() : super._(); @@ -967,6 +980,9 @@ abstract class PaymentDetails_Lightning extends PaymentDetails { /// The payment hash of the invoice String? get paymentHash; + /// The payment LNURL info + LnUrlInfo? get lnurlInfo; + /// For a Send swap which was refunded, this is the refund tx id String? get refundTxId; diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 8cb82f14c..45c6ce4c2 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -1060,6 +1060,17 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_cst_new_box_autoadd_ln_url_error_dataPtr .asFunction Function()>(); + ffi.Pointer frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info() { + return _frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info(); + } + + late final _frbgen_breez_liquid_cst_new_box_autoadd_ln_url_infoPtr = + _lookup Function()>>( + 'frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info'); + late final _frbgen_breez_liquid_cst_new_box_autoadd_ln_url_info = + _frbgen_breez_liquid_cst_new_box_autoadd_ln_url_infoPtr + .asFunction Function()>(); + ffi.Pointer frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_error_data() { return _frbgen_breez_liquid_cst_new_box_autoadd_ln_url_pay_error_data(); @@ -1503,6 +1514,21 @@ class FlutterBreezLiquidBindings { late final _frbgen_breez_liquid_cst_new_list_payment = _frbgen_breez_liquid_cst_new_list_paymentPtr .asFunction Function(int)>(); + ffi.Pointer frbgen_breez_liquid_cst_new_list_payment_state( + int len, + ) { + return _frbgen_breez_liquid_cst_new_list_payment_state( + len, + ); + } + + late final _frbgen_breez_liquid_cst_new_list_payment_statePtr = + _lookup Function(ffi.Int32)>>( + 'frbgen_breez_liquid_cst_new_list_payment_state'); + late final _frbgen_breez_liquid_cst_new_list_payment_state = + _frbgen_breez_liquid_cst_new_list_payment_statePtr + .asFunction Function(int)>(); + ffi.Pointer frbgen_breez_liquid_cst_new_list_payment_type( int len, ) { @@ -3862,6 +3888,26 @@ class FlutterBreezLiquidBindings { late final _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_hmac_sha256 = _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_hmac_sha256Ptr.asFunction(); + int uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encrypt() { + return _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encrypt(); + } + + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encryptPtr = + _lookup>( + 'uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encrypt'); + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encrypt = + _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_encryptPtr.asFunction(); + + int uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decrypt() { + return _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decrypt(); + } + + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decryptPtr = + _lookup>( + 'uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decrypt'); + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decrypt = + _uniffi_breez_sdk_liquid_bindings_checksum_method_signer_ecies_decryptPtr.asFunction(); + int ffi_breez_sdk_liquid_bindings_uniffi_contract_version() { return _ffi_breez_sdk_liquid_bindings_uniffi_contract_version(); } @@ -3949,6 +3995,13 @@ final class wire_cst_list_payment_type extends ffi.Struct { external int len; } +final class wire_cst_list_payment_state extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_ListPaymentDetails_Liquid extends ffi.Struct { external ffi.Pointer destination; } @@ -3973,6 +4026,8 @@ final class wire_cst_list_payment_details extends ffi.Struct { final class wire_cst_list_payments_request extends ffi.Struct { external ffi.Pointer filters; + external ffi.Pointer states; + external ffi.Pointer from_timestamp; external ffi.Pointer to_timestamp; @@ -4167,6 +4222,30 @@ final class wire_cst_send_destination extends ffi.Struct { external SendDestinationKind kind; } +final class wire_cst_ln_url_pay_request_data extends ffi.Struct { + external ffi.Pointer callback; + + @ffi.Uint64() + external int min_sendable; + + @ffi.Uint64() + external int max_sendable; + + external ffi.Pointer metadata_str; + + @ffi.Uint16() + external int comment_allowed; + + external ffi.Pointer domain; + + @ffi.Bool() + external bool allows_nostr; + + external ffi.Pointer nostr_pubkey; + + external ffi.Pointer ln_address; +} + final class wire_cst_aes_success_action_data extends ffi.Struct { external ffi.Pointer description; @@ -4221,6 +4300,10 @@ final class wire_cst_prepare_ln_url_pay_response extends ffi.Struct { @ffi.Uint64() external int fees_sat; + external wire_cst_ln_url_pay_request_data data; + + external ffi.Pointer comment; + external ffi.Pointer success_action; } @@ -4276,30 +4359,6 @@ final class wire_cst_prepare_buy_bitcoin_request extends ffi.Struct { external int amount_sat; } -final class wire_cst_ln_url_pay_request_data extends ffi.Struct { - external ffi.Pointer callback; - - @ffi.Uint64() - external int min_sendable; - - @ffi.Uint64() - external int max_sendable; - - external ffi.Pointer metadata_str; - - @ffi.Uint16() - external int comment_allowed; - - external ffi.Pointer domain; - - @ffi.Bool() - external bool allows_nostr; - - external ffi.Pointer nostr_pubkey; - - external ffi.Pointer ln_address; -} - final class wire_cst_prepare_ln_url_pay_request extends ffi.Struct { external wire_cst_ln_url_pay_request_data data; @@ -4411,6 +4470,76 @@ final class wire_cst_binding_event_listener extends ffi.Struct { external ffi.Pointer stream; } +final class wire_cst_aes_success_action_data_decrypted extends ffi.Struct { + external ffi.Pointer description; + + external ffi.Pointer plaintext; +} + +final class wire_cst_AesSuccessActionDataResult_Decrypted extends ffi.Struct { + external ffi.Pointer data; +} + +final class wire_cst_AesSuccessActionDataResult_ErrorStatus extends ffi.Struct { + external ffi.Pointer reason; +} + +final class AesSuccessActionDataResultKind extends ffi.Union { + external wire_cst_AesSuccessActionDataResult_Decrypted Decrypted; + + external wire_cst_AesSuccessActionDataResult_ErrorStatus ErrorStatus; +} + +final class wire_cst_aes_success_action_data_result extends ffi.Struct { + @ffi.Int32() + external int tag; + + external AesSuccessActionDataResultKind kind; +} + +final class wire_cst_SuccessActionProcessed_Aes extends ffi.Struct { + external ffi.Pointer result; +} + +final class wire_cst_SuccessActionProcessed_Message extends ffi.Struct { + external ffi.Pointer data; +} + +final class wire_cst_SuccessActionProcessed_Url extends ffi.Struct { + external ffi.Pointer data; +} + +final class SuccessActionProcessedKind extends ffi.Union { + external wire_cst_SuccessActionProcessed_Aes Aes; + + external wire_cst_SuccessActionProcessed_Message Message; + + external wire_cst_SuccessActionProcessed_Url Url; +} + +final class wire_cst_success_action_processed extends ffi.Struct { + @ffi.Int32() + external int tag; + + external SuccessActionProcessedKind kind; +} + +final class wire_cst_ln_url_info extends ffi.Struct { + external ffi.Pointer ln_address; + + external ffi.Pointer lnurl_pay_comment; + + external ffi.Pointer lnurl_pay_domain; + + external ffi.Pointer lnurl_pay_metadata; + + external ffi.Pointer lnurl_pay_success_action; + + external ffi.Pointer lnurl_pay_unprocessed_success_action; + + external ffi.Pointer lnurl_withdraw_endpoint; +} + final class wire_cst_PaymentDetails_Lightning extends ffi.Struct { external ffi.Pointer swap_id; @@ -4424,6 +4553,8 @@ final class wire_cst_PaymentDetails_Lightning extends ffi.Struct { external ffi.Pointer payment_hash; + external ffi.Pointer lnurl_info; + external ffi.Pointer refund_tx_id; external ffi.Pointer refund_tx_amount_sat; @@ -4565,6 +4696,8 @@ final class wire_cst_config extends ffi.Struct { @ffi.Uint32() external int zero_conf_min_fee_rate_msat; + external ffi.Pointer sync_service_url; + external ffi.Pointer zero_conf_max_amount_sat; external ffi.Pointer breez_api_key; @@ -4581,33 +4714,6 @@ final class wire_cst_connect_request extends ffi.Struct { external ffi.Pointer mnemonic; } -final class wire_cst_aes_success_action_data_decrypted extends ffi.Struct { - external ffi.Pointer description; - - external ffi.Pointer plaintext; -} - -final class wire_cst_AesSuccessActionDataResult_Decrypted extends ffi.Struct { - external ffi.Pointer data; -} - -final class wire_cst_AesSuccessActionDataResult_ErrorStatus extends ffi.Struct { - external ffi.Pointer reason; -} - -final class AesSuccessActionDataResultKind extends ffi.Union { - external wire_cst_AesSuccessActionDataResult_Decrypted Decrypted; - - external wire_cst_AesSuccessActionDataResult_ErrorStatus ErrorStatus; -} - -final class wire_cst_aes_success_action_data_result extends ffi.Struct { - @ffi.Int32() - external int tag; - - external AesSuccessActionDataResultKind kind; -} - final class wire_cst_bitcoin_address_data extends ffi.Struct { external ffi.Pointer address; @@ -4631,33 +4737,6 @@ final class wire_cst_ln_url_pay_error_data extends ffi.Struct { external ffi.Pointer reason; } -final class wire_cst_SuccessActionProcessed_Aes extends ffi.Struct { - external ffi.Pointer result; -} - -final class wire_cst_SuccessActionProcessed_Message extends ffi.Struct { - external ffi.Pointer data; -} - -final class wire_cst_SuccessActionProcessed_Url extends ffi.Struct { - external ffi.Pointer data; -} - -final class SuccessActionProcessedKind extends ffi.Union { - external wire_cst_SuccessActionProcessed_Aes Aes; - - external wire_cst_SuccessActionProcessed_Message Message; - - external wire_cst_SuccessActionProcessed_Url Url; -} - -final class wire_cst_success_action_processed extends ffi.Struct { - @ffi.Int32() - external int tag; - - external SuccessActionProcessedKind kind; -} - final class wire_cst_ln_url_pay_success_data extends ffi.Struct { external wire_cst_payment payment; diff --git a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt index cb5f53f34..3cfb3c54e 100644 --- a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt +++ b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt @@ -248,6 +248,7 @@ fun asConfig(config: ReadableMap): Config? { "network", "paymentTimeoutSec", "zeroConfMinFeeRateMsat", + "syncServiceUrl", "useDefaultExternalInputParsers", ), ) @@ -261,6 +262,7 @@ fun asConfig(config: ReadableMap): Config? { val network = config.getString("network")?.let { asLiquidNetwork(it) }!! val paymentTimeoutSec = config.getDouble("paymentTimeoutSec").toULong() val zeroConfMinFeeRateMsat = config.getInt("zeroConfMinFeeRateMsat").toUInt() + val syncServiceUrl = config.getString("syncServiceUrl")!! val breezApiKey = if (hasNonNullKey(config, "breezApiKey")) config.getString("breezApiKey") else null val cacheDir = if (hasNonNullKey(config, "cacheDir")) config.getString("cacheDir") else null val zeroConfMaxAmountSat = @@ -290,6 +292,7 @@ fun asConfig(config: ReadableMap): Config? { network, paymentTimeoutSec, zeroConfMinFeeRateMsat, + syncServiceUrl, breezApiKey, cacheDir, zeroConfMaxAmountSat, @@ -307,6 +310,7 @@ fun readableMapOf(config: Config): ReadableMap = "network" to config.network.name.lowercase(), "paymentTimeoutSec" to config.paymentTimeoutSec, "zeroConfMinFeeRateMsat" to config.zeroConfMinFeeRateMsat, + "syncServiceUrl" to config.syncServiceUrl, "breezApiKey" to config.breezApiKey, "cacheDir" to config.cacheDir, "zeroConfMaxAmountSat" to config.zeroConfMaxAmountSat, @@ -789,6 +793,14 @@ fun asListPaymentsRequest(listPaymentsRequest: ReadableMap): ListPaymentsRequest } else { null } + val states = + if (hasNonNullKey(listPaymentsRequest, "states")) { + listPaymentsRequest.getArray("states")?.let { + asPaymentStateList(it) + } + } else { + null + } val fromTimestamp = if (hasNonNullKey( listPaymentsRequest, @@ -810,12 +822,13 @@ fun asListPaymentsRequest(listPaymentsRequest: ReadableMap): ListPaymentsRequest } else { null } - return ListPaymentsRequest(filters, fromTimestamp, toTimestamp, offset, limit, details) + return ListPaymentsRequest(filters, states, fromTimestamp, toTimestamp, offset, limit, details) } fun readableMapOf(listPaymentsRequest: ListPaymentsRequest): ReadableMap = readableMapOf( "filters" to listPaymentsRequest.filters?.let { readableArrayOf(it) }, + "states" to listPaymentsRequest.states?.let { readableArrayOf(it) }, "fromTimestamp" to listPaymentsRequest.fromTimestamp, "toTimestamp" to listPaymentsRequest.toTimestamp, "offset" to listPaymentsRequest.offset, @@ -932,6 +945,81 @@ fun asLnUrlErrorDataList(arr: ReadableArray): List { return list } +fun asLnUrlInfo(lnUrlInfo: ReadableMap): LnUrlInfo? { + if (!validateMandatoryFields( + lnUrlInfo, + arrayOf(), + ) + ) { + return null + } + val lnAddress = if (hasNonNullKey(lnUrlInfo, "lnAddress")) lnUrlInfo.getString("lnAddress") else null + val lnurlPayComment = if (hasNonNullKey(lnUrlInfo, "lnurlPayComment")) lnUrlInfo.getString("lnurlPayComment") else null + val lnurlPayDomain = if (hasNonNullKey(lnUrlInfo, "lnurlPayDomain")) lnUrlInfo.getString("lnurlPayDomain") else null + val lnurlPayMetadata = if (hasNonNullKey(lnUrlInfo, "lnurlPayMetadata")) lnUrlInfo.getString("lnurlPayMetadata") else null + val lnurlPaySuccessAction = + if (hasNonNullKey(lnUrlInfo, "lnurlPaySuccessAction")) { + lnUrlInfo.getMap("lnurlPaySuccessAction")?.let { + asSuccessActionProcessed(it) + } + } else { + null + } + val lnurlPayUnprocessedSuccessAction = + if (hasNonNullKey( + lnUrlInfo, + "lnurlPayUnprocessedSuccessAction", + ) + ) { + lnUrlInfo.getMap("lnurlPayUnprocessedSuccessAction")?.let { + asSuccessAction(it) + } + } else { + null + } + val lnurlWithdrawEndpoint = + if (hasNonNullKey( + lnUrlInfo, + "lnurlWithdrawEndpoint", + ) + ) { + lnUrlInfo.getString("lnurlWithdrawEndpoint") + } else { + null + } + return LnUrlInfo( + lnAddress, + lnurlPayComment, + lnurlPayDomain, + lnurlPayMetadata, + lnurlPaySuccessAction, + lnurlPayUnprocessedSuccessAction, + lnurlWithdrawEndpoint, + ) +} + +fun readableMapOf(lnUrlInfo: LnUrlInfo): ReadableMap = + readableMapOf( + "lnAddress" to lnUrlInfo.lnAddress, + "lnurlPayComment" to lnUrlInfo.lnurlPayComment, + "lnurlPayDomain" to lnUrlInfo.lnurlPayDomain, + "lnurlPayMetadata" to lnUrlInfo.lnurlPayMetadata, + "lnurlPaySuccessAction" to lnUrlInfo.lnurlPaySuccessAction?.let { readableMapOf(it) }, + "lnurlPayUnprocessedSuccessAction" to lnUrlInfo.lnurlPayUnprocessedSuccessAction?.let { readableMapOf(it) }, + "lnurlWithdrawEndpoint" to lnUrlInfo.lnurlWithdrawEndpoint, + ) + +fun asLnUrlInfoList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toList()) { + when (value) { + is ReadableMap -> list.add(asLnUrlInfo(value)!!) + else -> throw SdkException.Generic(errUnexpectedType(value)) + } + } + return list +} + fun asLnUrlPayErrorData(lnUrlPayErrorData: ReadableMap): LnUrlPayErrorData? { if (!validateMandatoryFields( lnUrlPayErrorData, @@ -1572,6 +1660,7 @@ fun asPrepareLnUrlPayResponse(prepareLnUrlPayResponse: ReadableMap): PrepareLnUr arrayOf( "destination", "feesSat", + "data", ), ) ) { @@ -1579,6 +1668,8 @@ fun asPrepareLnUrlPayResponse(prepareLnUrlPayResponse: ReadableMap): PrepareLnUr } val destination = prepareLnUrlPayResponse.getMap("destination")?.let { asSendDestination(it) }!! val feesSat = prepareLnUrlPayResponse.getDouble("feesSat").toULong() + val data = prepareLnUrlPayResponse.getMap("data")?.let { asLnUrlPayRequestData(it) }!! + val comment = if (hasNonNullKey(prepareLnUrlPayResponse, "comment")) prepareLnUrlPayResponse.getString("comment") else null val successAction = if (hasNonNullKey(prepareLnUrlPayResponse, "successAction")) { prepareLnUrlPayResponse.getMap("successAction")?.let { @@ -1587,13 +1678,15 @@ fun asPrepareLnUrlPayResponse(prepareLnUrlPayResponse: ReadableMap): PrepareLnUr } else { null } - return PrepareLnUrlPayResponse(destination, feesSat, successAction) + return PrepareLnUrlPayResponse(destination, feesSat, data, comment, successAction) } fun readableMapOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse): ReadableMap = readableMapOf( "destination" to readableMapOf(prepareLnUrlPayResponse.destination), "feesSat" to prepareLnUrlPayResponse.feesSat, + "data" to readableMapOf(prepareLnUrlPayResponse.data), + "comment" to prepareLnUrlPayResponse.comment, "successAction" to prepareLnUrlPayResponse.successAction?.let { readableMapOf(it) }, ) @@ -2970,6 +3063,16 @@ fun asPaymentDetails(paymentDetails: ReadableMap): PaymentDetails? { val bolt11 = if (hasNonNullKey(paymentDetails, "bolt11")) paymentDetails.getString("bolt11") else null val bolt12Offer = if (hasNonNullKey(paymentDetails, "bolt12Offer")) paymentDetails.getString("bolt12Offer") else null val paymentHash = if (hasNonNullKey(paymentDetails, "paymentHash")) paymentDetails.getString("paymentHash") else null + val lnurlInfo = + if (hasNonNullKey( + paymentDetails, + "lnurlInfo", + ) + ) { + paymentDetails.getMap("lnurlInfo")?.let { asLnUrlInfo(it) } + } else { + null + } val refundTxId = if (hasNonNullKey(paymentDetails, "refundTxId")) paymentDetails.getString("refundTxId") else null val refundTxAmountSat = if (hasNonNullKey( @@ -2981,7 +3084,17 @@ fun asPaymentDetails(paymentDetails: ReadableMap): PaymentDetails? { } else { null } - return PaymentDetails.Lightning(swapId, description, preimage, bolt11, bolt12Offer, paymentHash, refundTxId, refundTxAmountSat) + return PaymentDetails.Lightning( + swapId, + description, + preimage, + bolt11, + bolt12Offer, + paymentHash, + lnurlInfo, + refundTxId, + refundTxAmountSat, + ) } if (type == "liquid") { val destination = paymentDetails.getString("destination")!! @@ -3018,6 +3131,7 @@ fun readableMapOf(paymentDetails: PaymentDetails): ReadableMap? { pushToMap(map, "bolt11", paymentDetails.bolt11) pushToMap(map, "bolt12Offer", paymentDetails.bolt12Offer) pushToMap(map, "paymentHash", paymentDetails.paymentHash) + pushToMap(map, "lnurlInfo", paymentDetails.lnurlInfo?.let { readableMapOf(it) }) pushToMap(map, "refundTxId", paymentDetails.refundTxId) pushToMap(map, "refundTxAmountSat", paymentDetails.refundTxAmountSat) } @@ -3347,6 +3461,7 @@ fun pushToArray( is LocaleOverrides -> array.pushMap(readableMapOf(value)) is LocalizedName -> array.pushMap(readableMapOf(value)) is Payment -> array.pushMap(readableMapOf(value)) + is PaymentState -> array.pushString(value.name.lowercase()) is PaymentType -> array.pushString(value.name.lowercase()) is Rate -> array.pushMap(readableMapOf(value)) is RefundableSwap -> array.pushMap(readableMapOf(value)) diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index e1bdbaaa2..3e3bb72e6 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -307,6 +307,9 @@ enum BreezSDKLiquidMapper { guard let zeroConfMinFeeRateMsat = config["zeroConfMinFeeRateMsat"] as? UInt32 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "zeroConfMinFeeRateMsat", typeName: "Config")) } + guard let syncServiceUrl = config["syncServiceUrl"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "syncServiceUrl", typeName: "Config")) + } var breezApiKey: String? if hasNonNilKey(data: config, key: "breezApiKey") { guard let breezApiKeyTmp = config["breezApiKey"] as? String else { @@ -336,7 +339,7 @@ enum BreezSDKLiquidMapper { externalInputParsers = try asExternalInputParserList(arr: externalInputParsersTmp) } - return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, zeroConfMinFeeRateMsat: zeroConfMinFeeRateMsat, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers) + return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, zeroConfMinFeeRateMsat: zeroConfMinFeeRateMsat, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers) } static func dictionaryOf(config: Config) -> [String: Any?] { @@ -348,6 +351,7 @@ enum BreezSDKLiquidMapper { "network": valueOf(liquidNetwork: config.network), "paymentTimeoutSec": config.paymentTimeoutSec, "zeroConfMinFeeRateMsat": config.zeroConfMinFeeRateMsat, + "syncServiceUrl": config.syncServiceUrl, "breezApiKey": config.breezApiKey == nil ? nil : config.breezApiKey, "cacheDir": config.cacheDir == nil ? nil : config.cacheDir, "zeroConfMaxAmountSat": config.zeroConfMaxAmountSat == nil ? nil : config.zeroConfMaxAmountSat, @@ -952,6 +956,11 @@ enum BreezSDKLiquidMapper { filters = try asPaymentTypeList(arr: filtersTmp) } + var states: [PaymentState]? + if let statesTmp = listPaymentsRequest["states"] as? [String] { + states = try asPaymentStateList(arr: statesTmp) + } + var fromTimestamp: Int64? if hasNonNilKey(data: listPaymentsRequest, key: "fromTimestamp") { guard let fromTimestampTmp = listPaymentsRequest["fromTimestamp"] as? Int64 else { @@ -985,12 +994,13 @@ enum BreezSDKLiquidMapper { details = try asListPaymentDetails(listPaymentDetails: detailsTmp) } - return ListPaymentsRequest(filters: filters, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, offset: offset, limit: limit, details: details) + return ListPaymentsRequest(filters: filters, states: states, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, offset: offset, limit: limit, details: details) } static func dictionaryOf(listPaymentsRequest: ListPaymentsRequest) -> [String: Any?] { return [ "filters": listPaymentsRequest.filters == nil ? nil : arrayOf(paymentTypeList: listPaymentsRequest.filters!), + "states": listPaymentsRequest.states == nil ? nil : arrayOf(paymentStateList: listPaymentsRequest.states!), "fromTimestamp": listPaymentsRequest.fromTimestamp == nil ? nil : listPaymentsRequest.fromTimestamp, "toTimestamp": listPaymentsRequest.toTimestamp == nil ? nil : listPaymentsRequest.toTimestamp, "offset": listPaymentsRequest.offset == nil ? nil : listPaymentsRequest.offset, @@ -1125,6 +1135,85 @@ enum BreezSDKLiquidMapper { return lnUrlErrorDataList.map { v -> [String: Any?] in return dictionaryOf(lnUrlErrorData: v) } } + static func asLnUrlInfo(lnUrlInfo: [String: Any?]) throws -> LnUrlInfo { + var lnAddress: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnAddress") { + guard let lnAddressTmp = lnUrlInfo["lnAddress"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnAddress")) + } + lnAddress = lnAddressTmp + } + var lnurlPayComment: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayComment") { + guard let lnurlPayCommentTmp = lnUrlInfo["lnurlPayComment"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayComment")) + } + lnurlPayComment = lnurlPayCommentTmp + } + var lnurlPayDomain: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayDomain") { + guard let lnurlPayDomainTmp = lnUrlInfo["lnurlPayDomain"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayDomain")) + } + lnurlPayDomain = lnurlPayDomainTmp + } + var lnurlPayMetadata: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayMetadata") { + guard let lnurlPayMetadataTmp = lnUrlInfo["lnurlPayMetadata"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayMetadata")) + } + lnurlPayMetadata = lnurlPayMetadataTmp + } + var lnurlPaySuccessAction: SuccessActionProcessed? + if let lnurlPaySuccessActionTmp = lnUrlInfo["lnurlPaySuccessAction"] as? [String: Any?] { + lnurlPaySuccessAction = try asSuccessActionProcessed(successActionProcessed: lnurlPaySuccessActionTmp) + } + + var lnurlPayUnprocessedSuccessAction: SuccessAction? + if let lnurlPayUnprocessedSuccessActionTmp = lnUrlInfo["lnurlPayUnprocessedSuccessAction"] as? [String: Any?] { + lnurlPayUnprocessedSuccessAction = try asSuccessAction(successAction: lnurlPayUnprocessedSuccessActionTmp) + } + + var lnurlWithdrawEndpoint: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlWithdrawEndpoint") { + guard let lnurlWithdrawEndpointTmp = lnUrlInfo["lnurlWithdrawEndpoint"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlWithdrawEndpoint")) + } + lnurlWithdrawEndpoint = lnurlWithdrawEndpointTmp + } + + return LnUrlInfo(lnAddress: lnAddress, lnurlPayComment: lnurlPayComment, lnurlPayDomain: lnurlPayDomain, lnurlPayMetadata: lnurlPayMetadata, lnurlPaySuccessAction: lnurlPaySuccessAction, lnurlPayUnprocessedSuccessAction: lnurlPayUnprocessedSuccessAction, lnurlWithdrawEndpoint: lnurlWithdrawEndpoint) + } + + static func dictionaryOf(lnUrlInfo: LnUrlInfo) -> [String: Any?] { + return [ + "lnAddress": lnUrlInfo.lnAddress == nil ? nil : lnUrlInfo.lnAddress, + "lnurlPayComment": lnUrlInfo.lnurlPayComment == nil ? nil : lnUrlInfo.lnurlPayComment, + "lnurlPayDomain": lnUrlInfo.lnurlPayDomain == nil ? nil : lnUrlInfo.lnurlPayDomain, + "lnurlPayMetadata": lnUrlInfo.lnurlPayMetadata == nil ? nil : lnUrlInfo.lnurlPayMetadata, + "lnurlPaySuccessAction": lnUrlInfo.lnurlPaySuccessAction == nil ? nil : dictionaryOf(successActionProcessed: lnUrlInfo.lnurlPaySuccessAction!), + "lnurlPayUnprocessedSuccessAction": lnUrlInfo.lnurlPayUnprocessedSuccessAction == nil ? nil : dictionaryOf(successAction: lnUrlInfo.lnurlPayUnprocessedSuccessAction!), + "lnurlWithdrawEndpoint": lnUrlInfo.lnurlWithdrawEndpoint == nil ? nil : lnUrlInfo.lnurlWithdrawEndpoint, + ] + } + + static func asLnUrlInfoList(arr: [Any]) throws -> [LnUrlInfo] { + var list = [LnUrlInfo]() + for value in arr { + if let val = value as? [String: Any?] { + var lnUrlInfo = try asLnUrlInfo(lnUrlInfo: val) + list.append(lnUrlInfo) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "LnUrlInfo")) + } + } + return list + } + + static func arrayOf(lnUrlInfoList: [LnUrlInfo]) -> [Any] { + return lnUrlInfoList.map { v -> [String: Any?] in return dictionaryOf(lnUrlInfo: v) } + } + static func asLnUrlPayErrorData(lnUrlPayErrorData: [String: Any?]) throws -> LnUrlPayErrorData { guard let paymentHash = lnUrlPayErrorData["paymentHash"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentHash", typeName: "LnUrlPayErrorData")) @@ -1865,18 +1954,32 @@ enum BreezSDKLiquidMapper { guard let feesSat = prepareLnUrlPayResponse["feesSat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "feesSat", typeName: "PrepareLnUrlPayResponse")) } + guard let dataTmp = prepareLnUrlPayResponse["data"] as? [String: Any?] else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "data", typeName: "PrepareLnUrlPayResponse")) + } + let data = try asLnUrlPayRequestData(lnUrlPayRequestData: dataTmp) + + var comment: String? + if hasNonNilKey(data: prepareLnUrlPayResponse, key: "comment") { + guard let commentTmp = prepareLnUrlPayResponse["comment"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "comment")) + } + comment = commentTmp + } var successAction: SuccessAction? if let successActionTmp = prepareLnUrlPayResponse["successAction"] as? [String: Any?] { successAction = try asSuccessAction(successAction: successActionTmp) } - return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, successAction: successAction) + return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, data: data, comment: comment, successAction: successAction) } static func dictionaryOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse) -> [String: Any?] { return [ "destination": dictionaryOf(sendDestination: prepareLnUrlPayResponse.destination), "feesSat": prepareLnUrlPayResponse.feesSat, + "data": dictionaryOf(lnUrlPayRequestData: prepareLnUrlPayResponse.data), + "comment": prepareLnUrlPayResponse.comment == nil ? nil : prepareLnUrlPayResponse.comment, "successAction": prepareLnUrlPayResponse.successAction == nil ? nil : dictionaryOf(successAction: prepareLnUrlPayResponse.successAction!), ] } @@ -3653,11 +3756,16 @@ enum BreezSDKLiquidMapper { let _paymentHash = paymentDetails["paymentHash"] as? String + var _lnurlInfo: LnUrlInfo? + if let lnurlInfoTmp = paymentDetails["lnurlInfo"] as? [String: Any?] { + _lnurlInfo = try asLnUrlInfo(lnUrlInfo: lnurlInfoTmp) + } + let _refundTxId = paymentDetails["refundTxId"] as? String let _refundTxAmountSat = paymentDetails["refundTxAmountSat"] as? UInt64 - return PaymentDetails.lightning(swapId: _swapId, description: _description, preimage: _preimage, bolt11: _bolt11, bolt12Offer: _bolt12Offer, paymentHash: _paymentHash, refundTxId: _refundTxId, refundTxAmountSat: _refundTxAmountSat) + return PaymentDetails.lightning(swapId: _swapId, description: _description, preimage: _preimage, bolt11: _bolt11, bolt12Offer: _bolt12Offer, paymentHash: _paymentHash, lnurlInfo: _lnurlInfo, refundTxId: _refundTxId, refundTxAmountSat: _refundTxAmountSat) } if type == "liquid" { guard let _destination = paymentDetails["destination"] as? String else { @@ -3688,7 +3796,7 @@ enum BreezSDKLiquidMapper { static func dictionaryOf(paymentDetails: PaymentDetails) -> [String: Any?] { switch paymentDetails { case let .lightning( - swapId, description, preimage, bolt11, bolt12Offer, paymentHash, refundTxId, refundTxAmountSat + swapId, description, preimage, bolt11, bolt12Offer, paymentHash, lnurlInfo, refundTxId, refundTxAmountSat ): return [ "type": "lightning", @@ -3698,6 +3806,7 @@ enum BreezSDKLiquidMapper { "bolt11": bolt11 == nil ? nil : bolt11, "bolt12Offer": bolt12Offer == nil ? nil : bolt12Offer, "paymentHash": paymentHash == nil ? nil : paymentHash, + "lnurlInfo": lnurlInfo == nil ? nil : dictionaryOf(lnUrlInfo: lnurlInfo!), "refundTxId": refundTxId == nil ? nil : refundTxId, "refundTxAmountSat": refundTxAmountSat == nil ? nil : refundTxAmountSat, ] diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index a89e816f1..12f1e5921 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -65,6 +65,7 @@ export interface Config { network: LiquidNetwork paymentTimeoutSec: number zeroConfMinFeeRateMsat: number + syncServiceUrl: string breezApiKey?: string cacheDir?: string zeroConfMaxAmountSat?: number @@ -158,6 +159,7 @@ export interface LiquidAddressData { export interface ListPaymentsRequest { filters?: PaymentType[] + states?: PaymentState[] fromTimestamp?: number toTimestamp?: number offset?: number @@ -180,6 +182,16 @@ export interface LnUrlErrorData { reason: string } +export interface LnUrlInfo { + lnAddress?: string + lnurlPayComment?: string + lnurlPayDomain?: string + lnurlPayMetadata?: string + lnurlPaySuccessAction?: SuccessActionProcessed + lnurlPayUnprocessedSuccessAction?: SuccessAction + lnurlWithdrawEndpoint?: string +} + export interface LnUrlPayErrorData { paymentHash: string reason: string @@ -287,6 +299,8 @@ export interface PrepareLnUrlPayRequest { export interface PrepareLnUrlPayResponse { destination: SendDestination feesSat: number + data: LnUrlPayRequestData + comment?: string successAction?: SuccessAction } @@ -605,6 +619,7 @@ export type PaymentDetails = { bolt11?: string bolt12Offer?: string paymentHash?: string + lnurlInfo?: LnUrlInfo refundTxId?: string refundTxAmountSat?: number } | {