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 3c57d571c..206a8f8d0 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -194,6 +194,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "asn1-rs" version = "0.6.2" @@ -218,7 +224,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", "synstructure", ] @@ -230,7 +236,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -252,7 +258,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -263,7 +269,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -302,7 +308,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", @@ -318,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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +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.1", "tower-layer", "tower-service", ] @@ -340,6 +373,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.73" @@ -582,6 +635,7 @@ dependencies = [ "bip39", "boltz-client", "chrono", + "ecies", "electrum-client", "env_logger 0.11.5", "flutter_rust_bridge", @@ -593,6 +647,7 @@ dependencies = [ "lwk_signer", "lwk_wollet", "openssl", + "prost 0.13.3", "reqwest 0.11.20", "rusqlite", "rusqlite_migration", @@ -608,7 +663,10 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "tonic 0.12.3", + "tonic-build 0.12.3", "url", + "uuid", "x509-parser", "zbase32", ] @@ -732,7 +790,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -800,6 +858,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" @@ -887,7 +957,7 @@ checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -913,6 +983,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" @@ -932,7 +1011,24 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", +] + +[[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]] @@ -1149,7 +1245,7 @@ dependencies = [ "md-5", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -1238,7 +1334,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -1292,9 +1388,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", @@ -1428,13 +1524,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]] @@ -1566,6 +1671,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1603,6 +1709,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1631,7 +1750,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -1772,6 +1891,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", + "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" @@ -1952,7 +2116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2134,6 +2298,10 @@ name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -2164,7 +2332,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -2214,7 +2382,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", ] [[package]] @@ -2226,11 +2404,24 @@ dependencies = [ "cfg-if", "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "winapi", ] +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.7", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2270,7 +2461,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -2303,6 +2494,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2328,6 +2525,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" +dependencies = [ + "proc-macro2", + "syn 2.0.82", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -2344,7 +2551,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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +dependencies = [ + "bytes", + "prost-derive 0.13.3", ] [[package]] @@ -2360,15 +2577,36 @@ 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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease 0.2.24", + "prost 0.13.3", + "prost-types 0.13.3", + "regex", + "syn 2.0.82", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -2382,13 +2620,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" +dependencies = [ + "prost 0.13.3", ] [[package]] @@ -2524,6 +2784,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" version = "1.10.6" @@ -2864,7 +3133,7 @@ checksum = "e5af959c8bf6af1aff6d2b463a57f71aae53d1332da58419e30ad8dc7011d951" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -2914,7 +3183,7 @@ dependencies = [ "lightning 0.0.118", "lightning-invoice 0.26.0", "log", - "prost", + "prost 0.11.9", "querystring", "regex", "reqwest 0.11.20", @@ -2922,8 +3191,8 @@ dependencies = [ "serde_json", "strum_macros", "thiserror", - "tonic", - "tonic-build", + "tonic 0.8.3", + "tonic-build 0.8.4", "url", "urlencoding", ] @@ -3067,7 +3336,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -3124,7 +3393,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3135,7 +3404,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3209,7 +3478,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -3231,9 +3500,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.75" +version = "2.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" dependencies = [ "proc-macro2", "quote", @@ -3263,7 +3532,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -3343,7 +3612,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -3435,7 +3704,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -3472,9 +3741,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -3517,7 +3786,7 @@ checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.13.1", "bytes", "futures-core", @@ -3526,18 +3795,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", @@ -3545,19 +3814,65 @@ 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.7", + "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.1", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.3", + "rustls-pemfile 2.1.3", + "socket2", + "tokio", + "tokio-rustls 0.26.0", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + [[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.24", + "proc-macro2", + "prost-build 0.13.3", + "prost-types 0.13.3", + "quote", + "syn 2.0.82", +] + [[package]] name = "tower" version = "0.4.13" @@ -3578,6 +3893,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3609,7 +3938,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] @@ -3766,6 +4095,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -3815,7 +4153,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", "wasm-bindgen-shared", ] @@ -3849,7 +4187,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3868,7 +4206,7 @@ checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" dependencies = [ "futures", "js-sys", - "parking_lot", + "parking_lot 0.11.2", "pin-utils", "wasm-bindgen", "wasm-bindgen-futures", @@ -4200,7 +4538,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.75", + "syn 2.0.82", ] [[package]] diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 39ff2d6e9..274a15a70 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -463,7 +463,7 @@ pub(crate) async fn handle_command( _ => None, }; - let payments = sdk + let mut payments = sdk .list_payments(&ListPaymentsRequest { filters, from_timestamp, @@ -473,6 +473,7 @@ pub(crate) async fn handle_command( details, }) .await?; + payments.reverse(); command_result!(payments) } Command::GetPayment { payment_hash } => { diff --git a/cli/src/main.rs b/cli/src/main.rs index 0a4a91941..e5549caea 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -75,6 +75,11 @@ async fn main() -> Result<()> { .map(|var| var.into_string().expect("Expected valid API key string")); let mut config = LiquidSdk::default_config(network, breez_api_key)?; config.working_dir = data_dir_str; + if let Some(sync_service_url) = std::env::var_os("SYNC_SERVICE_URL") { + config.sync_service_url = sync_service_url + .into_string() + .expect("Expected a valid SYNC_SERIVCE_URL"); + } let sdk = LiquidSdk::connect(ConnectRequest { mnemonic: mnemonic.to_string(), config, diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 43b828ff5..7b15fbb4b 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 = "askama" version = "0.11.1" @@ -376,7 +382,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", @@ -392,7 +398,34 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +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.1", "tower-layer", "tower-service", ] @@ -414,6 +447,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" @@ -680,6 +733,7 @@ dependencies = [ "bip39", "boltz-client", "chrono", + "ecies", "electrum-client", "env_logger 0.11.5", "flutter_rust_bridge", @@ -693,6 +747,7 @@ dependencies = [ "lwk_wollet", "openssl", "paste", + "prost 0.13.3", "reqwest 0.11.20", "rusqlite", "rusqlite_migration", @@ -709,6 +764,8 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite", + "tonic 0.12.3", + "tonic-build 0.12.3", "url", "uuid", "x509-parser", @@ -981,6 +1038,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" @@ -1094,6 +1163,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" @@ -1116,6 +1194,23 @@ dependencies = [ "syn 2.0.77", ] +[[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]] name = "either" version = "1.13.0" @@ -1465,9 +1560,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", @@ -1612,13 +1707,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]] @@ -1750,6 +1854,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1787,6 +1892,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1815,7 +1933,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -1975,6 +2093,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" @@ -2155,7 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2318,6 +2481,10 @@ name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "oneshot-uniffi" @@ -2534,6 +2701,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -2559,6 +2732,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.77", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2599,7 +2782,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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +dependencies = [ + "bytes", + "prost-derive 0.13.3", ] [[package]] @@ -2615,15 +2808,36 @@ 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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease 0.2.22", + "prost 0.13.3", + "prost-types 0.13.3", + "regex", + "syn 2.0.77", + "tempfile", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -2637,13 +2851,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" +dependencies = [ + "prost 0.13.3", ] [[package]] @@ -3194,7 +3430,7 @@ dependencies = [ "lightning 0.0.118", "lightning-invoice 0.26.0", "log", - "prost", + "prost 0.11.9", "querystring", "regex", "reqwest 0.11.20", @@ -3202,8 +3438,8 @@ dependencies = [ "serde_json", "strum_macros", "thiserror", - "tonic", - "tonic-build", + "tonic 0.8.3", + "tonic-build 0.8.4", "url", "urlencoding", ] @@ -3413,7 +3649,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3424,7 +3660,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3854,7 +4090,7 @@ checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.13.1", "bytes", "futures-core", @@ -3863,18 +4099,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", @@ -3882,19 +4118,65 @@ 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.7", + "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.1", + "hyper-util", + "percent-encoding", + "pin-project", + "prost 0.13.3", + "rustls-pemfile 2.1.3", + "socket2", + "tokio", + "tokio-rustls 0.26.0", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + [[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.22", + "proc-macro2", + "prost-build 0.13.3", + "prost-types 0.13.3", + "quote", + "syn 2.0.77", +] + [[package]] name = "tower" version = "0.4.13" @@ -3915,6 +4197,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" 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 d02b1bd78..0204a230d 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 @@ -442,6 +442,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; } wire_cst_config; diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 290267558..bbf6012bd 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -310,6 +310,7 @@ dictionary Config { LiquidNetwork network; u64 payment_timeout_sec; u32 zero_conf_min_fee_rate_msat; + string sync_service_url; string? breez_api_key; u64? zero_conf_max_amount_sat; }; diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 5fbce3d50..a010b9d9f 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -49,6 +49,10 @@ electrum-client = { version = "0.19.0" } zbase32 = "0.1.2" x509-parser = { version = "0.16.0" } tempfile = "3" +tonic = { version = "0.12.3", features = ["tls"] } +prost = "0.13.3" +ecies = "0.2.7" +uuid = { version = "1.8.0", features = ["v4"] } [dev-dependencies] lazy_static = "1.5.0" @@ -59,6 +63,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..37bc80270 100644 --- a/lib/core/build.rs +++ b/lib/core/build.rs @@ -32,7 +32,16 @@ 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"])?; + Ok(()) +} + fn main() -> Result<()> { setup_x86_64_android_workaround(); + compile_protos()?; Ok(()) } diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index 488621c8e..366ff6f6c 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -103,10 +103,63 @@ impl ChainSwapHandler { .persister .fetch_chain_swap_by_id(id)? .ok_or(anyhow!("No ongoing Chain Swap found for ID {id}"))?; + let swap_state = ChainSwapStates::from_str(&update.status).map_err(|_| { + anyhow!( + "Invalid ChainSwapState for Chain Swap {id}: {}", + update.status + ) + })?; + + // The following block deals with non-local (synced) outgoing chain swaps, which require + // some pre-processing steps before continuing with the flow + if !swap.sync_details.is_local { + match &swap_state { + // If the state is `Created`: + // 1. Outgoing case - we do not lockup twice + // 2. Incoming case - do nothing + ChainSwapStates::Created => { + debug!( + "Received state {swap_state:?} for non-local Chain swap {id}. Skipping..." + ); + return Ok(()); + } + + // If the state is `TransactionMempool` or `TransactionConfirmed` we need to + // continue and update the `user_lockup_tx_id` field + ChainSwapStates::TransactionMempool | ChainSwapStates::TransactionConfirmed => {} + + // If the state is `TransactionServerMempool`, we need to first try recovering the + // claim_tx_id to avoid broadcasting twice. If none is present, we continue below. + ChainSwapStates::TransactionServerMempool => { + // TODO Add claim_tx_id recovery logic + } + + // If the state is `TransactionServerConfirmed` we need to first try recovering the + // claim_tx_id to avoid broadcasting twice. If none is present, we continue below. + // By this point, we also need to ensure that `user_lockup_tx_id` is present. This + // will usually happen if the `TransactionMempool/TransactionConfirmed` states have + // been skipped. + ChainSwapStates::TransactionServerConfirmed => { + // TODO Add claim_tx_id and user_lockup_tx_id recovery logic + } + + // If the state is failed, we have to recover both user_lockup_tx_id and + // refund_tx_id, then continue with the flow below + ChainSwapStates::TransactionFailed + | ChainSwapStates::TransactionLockupFailed + | ChainSwapStates::TransactionRefunded + | ChainSwapStates::SwapExpired => { + // TODO Add user_lockup_tx_id and refund_tx_id recovery logic + } + + // For everything else, simply show the debug message + _ => {} + } + } match swap.direction { - Direction::Incoming => self.on_new_incoming_status(&swap, update).await, - Direction::Outgoing => self.on_new_outgoing_status(&swap, update).await, + Direction::Incoming => self.on_new_incoming_status(&swap, update, swap_state).await, + Direction::Outgoing => self.on_new_outgoing_status(&swap, update, swap_state).await, } } @@ -232,11 +285,14 @@ impl ChainSwapHandler { Ok(()) } - async fn on_new_incoming_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> { + async fn on_new_incoming_status( + &self, + swap: &ChainSwap, + update: &boltz::Update, + swap_state: ChainSwapStates, + ) -> Result<()> { let id = &update.id; let status = &update.status; - let swap_state = ChainSwapStates::from_str(status) - .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; info!("Handling incoming Chain Swap transition to {status:?} for swap {id}"); // See https://docs.boltz.exchange/v/api/lifecycle#chain-swaps @@ -380,11 +436,14 @@ impl ChainSwapHandler { } } - async fn on_new_outgoing_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> { + async fn on_new_outgoing_status( + &self, + swap: &ChainSwap, + update: &boltz::Update, + swap_state: ChainSwapStates, + ) -> Result<()> { let id = &update.id; let status = &update.status; - let swap_state = ChainSwapStates::from_str(status) - .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; info!("Handling outgoing Chain Swap transition to {status:?} for swap {id}"); // See https://docs.boltz.exchange/v/api/lifecycle#chain-swaps diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index 54c3f65aa..0723f7bb3 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -2286,6 +2286,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); return crate::model::Config { @@ -2296,6 +2297,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, }; @@ -4318,6 +4320,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(), ] @@ -6290,6 +6293,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); } @@ -8216,6 +8220,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(), } @@ -9573,6 +9578,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(), } @@ -11469,6 +11475,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, } diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index 5190ad22b..d55036f8f 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -182,6 +182,7 @@ 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 a4a4acba3..30eee72b5 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -17,16 +17,20 @@ use sdk_common::prelude::*; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; -use crate::error::{PaymentError, SdkError, SdkResult}; use crate::receive_swap::{ DEFAULT_ZERO_CONF_MAX_SAT, DEFAULT_ZERO_CONF_MIN_FEE_RATE_MAINNET, DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET, }; use crate::utils; +use crate::{ + error::{PaymentError, SdkError, SdkResult}, + sync::model::SyncDetails, +}; // 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; +pub const BREEZ_SYNC_SERVICE_URL: &str = "TODO"; /// Configuration for the Liquid SDK #[derive(Clone, Debug, Serialize)] @@ -44,6 +48,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 service used to synchronize data across devices + pub sync_service_url: String, /// Maximum amount in satoshi to accept zero-conf payments with /// Defaults to [crate::receive_swap::DEFAULT_ZERO_CONF_MAX_SAT] pub zero_conf_max_amount_sat: Option, @@ -63,6 +69,7 @@ impl Config { zero_conf_min_fee_rate_msat: DEFAULT_ZERO_CONF_MIN_FEE_RATE_MAINNET, zero_conf_max_amount_sat: None, breez_api_key: Some(breez_api_key), + sync_service_url: BREEZ_SYNC_SERVICE_URL.to_string(), } } @@ -77,6 +84,7 @@ impl Config { zero_conf_min_fee_rate_msat: DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET, zero_conf_max_amount_sat: None, breez_api_key, + sync_service_url: BREEZ_SYNC_SERVICE_URL.to_string(), } } @@ -248,6 +256,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: https://github.com/lnurl/luds/blob/luds/05.md fn hmac_sha256(&self, msg: Vec, derivation_path: String) -> Result, SignerError>; + + /// Encrypts a message using (ECIES)[ecies::encrypt] + fn ecies_encrypt(&self, msg: &[u8]) -> Result, SignerError>; + + /// Decrypts a message using (ECIES)[ecies::decrypt] + fn ecies_decrypt(&self, msg: &[u8]) -> Result, SignerError>; } /// An argument when calling [crate::sdk::LiquidSdk::connect]. @@ -569,7 +583,7 @@ impl SwapScriptV2 { } } -#[derive(Debug, Copy, Clone, PartialEq, Serialize)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum Direction { Incoming = 0, Outgoing = 1, @@ -622,6 +636,7 @@ pub(crate) struct ChainSwap { pub(crate) state: PaymentState, pub(crate) claim_private_key: String, pub(crate) refund_private_key: String, + pub(crate) sync_details: SyncDetails, } impl ChainSwap { pub(crate) fn get_claim_keypair(&self) -> SdkResult { @@ -742,6 +757,7 @@ pub(crate) struct SendSwap { pub(crate) created_at: u32, pub(crate) state: PaymentState, pub(crate) refund_private_key: String, + pub(crate) sync_details: SyncDetails, } impl SendSwap { pub(crate) fn get_refund_keypair(&self) -> Result { @@ -826,6 +842,7 @@ pub(crate) struct ReceiveSwap { /// Afterwards, it shows the lockup tx creation time. pub(crate) created_at: u32, pub(crate) state: PaymentState, + pub(crate) sync_details: SyncDetails, } impl ReceiveSwap { pub(crate) fn get_claim_keypair(&self) -> Result { diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index adbd5800a..aa767f18a 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -10,6 +10,7 @@ use crate::ensure_sdk; use crate::error::PaymentError; use crate::model::*; use crate::persist::{get_where_clause_state_in, Persister}; +use crate::sync::model::SyncDetails; impl Persister { pub(crate) fn insert_chain_swap(&self, chain_swap: &ChainSwap) -> Result<()> { @@ -19,7 +20,7 @@ impl Persister { // so we split up the insert into two statements. let mut stmt = con.prepare( " - INSERT INTO chain_swaps ( + INSERT OR REPLACE INTO chain_swaps ( id, id_hash, direction, @@ -66,7 +67,8 @@ impl Persister { 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 + refund_tx_id = :refund_tx_id, + is_local = :is_local WHERE id = :id", named_params! { @@ -76,43 +78,50 @@ impl Persister { ":user_lockup_tx_id": &chain_swap.user_lockup_tx_id, ":claim_tx_id": &chain_swap.claim_tx_id, ":refund_tx_id": &chain_swap.refund_tx_id, + ":is_local": &chain_swap.sync_details.is_local, }, )?; + self.insert_or_update_sync_details(&chain_swap.id, &chain_swap.sync_details)?; + Ok(()) } fn list_chain_swaps_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 = String::from(" AND "); where_clause_str.push_str(where_clauses.join(" AND ").as_str()); } format!( " SELECT - id, - direction, - claim_address, - lockup_address, - timeout_block_height, - preimage, - description, - payer_amount_sat, - receiver_amount_sat, - accept_zero_conf, - create_response_json, - claim_private_key, - refund_private_key, - server_lockup_tx_id, - user_lockup_tx_id, - claim_fees_sat, - claim_tx_id, - refund_tx_id, - created_at, - state - FROM chain_swaps + cs.id, + cs.direction, + cs.claim_address, + cs.lockup_address, + cs.timeout_block_height, + cs.preimage, + cs.description, + cs.payer_amount_sat, + cs.receiver_amount_sat, + cs.accept_zero_conf, + cs.create_response_json, + cs.claim_private_key, + cs.refund_private_key, + cs.server_lockup_tx_id, + cs.user_lockup_tx_id, + cs.claim_fees_sat, + cs.claim_tx_id, + cs.refund_tx_id, + cs.created_at, + cs.state, + sd.is_local, + sd.revision, + sd.record_id + FROM chain_swaps cs, sync_details sd + WHERE cs.id = sd.data_identifier {where_clause_str} ORDER BY created_at " @@ -160,6 +169,11 @@ impl Persister { refund_tx_id: row.get(17)?, created_at: row.get(18)?, state: row.get(19)?, + sync_details: SyncDetails { + is_local: row.get(20)?, + revision: row.get(21)?, + record_id: row.get(22)?, + }, }) } diff --git a/lib/core/src/persist/migrations.rs b/lib/core/src/persist/migrations.rs index 84138a3f6..b44ad45ea 100644 --- a/lib/core/src/persist/migrations.rs +++ b/lib/core/src/persist/migrations.rs @@ -98,5 +98,17 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { ALTER TABLE receive_swaps ADD COLUMN payment_hash TEXT; ALTER TABLE send_swaps ADD COLUMN payment_hash TEXT; ", + "CREATE TABLE IF NOT EXISTS pending_sync_records ( + id TEXT NOT NULL UNIQUE, + schema_version REAL NOT NULL, + data BLOB NOT NULL, + revision INTEGER NOT NULL PRIMARY KEY + ) STRICT;", + "CREATE TABLE IF NOT EXISTS sync_details ( + data_identifier TEXT NOT NULL PRIMARY KEY, + is_local INTEGER NOT NULL DEFAULT 1, + revision INTEGER, + record_id TEXT + ) STRICT;", ] } diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index 1446aa8e9..53948d29c 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod chain; mod migrations; pub(crate) mod receive; pub(crate) mod send; +pub(crate) mod sync; use std::collections::HashSet; use std::{fs::create_dir_all, path::PathBuf, str::FromStr}; diff --git a/lib/core/src/persist/receive.rs b/lib/core/src/persist/receive.rs index 245a89260..8f7c91c28 100644 --- a/lib/core/src/persist/receive.rs +++ b/lib/core/src/persist/receive.rs @@ -10,6 +10,7 @@ use crate::ensure_sdk; use crate::error::PaymentError; use crate::model::*; use crate::persist::{get_where_clause_state_in, Persister}; +use crate::sync::model::SyncDetails; impl Persister { pub(crate) fn insert_receive_swap(&self, receive_swap: &ReceiveSwap) -> Result<()> { @@ -17,7 +18,7 @@ impl Persister { let mut stmt = con.prepare( " - INSERT INTO receive_swaps ( + INSERT OR REPLACE INTO receive_swaps ( id, id_hash, preimage, @@ -53,13 +54,15 @@ impl Persister { &receive_swap.state, ))?; + self.insert_or_update_sync_details(&receive_swap.id, &receive_swap.sync_details)?; + Ok(()) } fn list_receive_swaps_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 = String::from(" AND "); where_clause_str.push_str(where_clauses.join(" AND ").as_str()); } @@ -78,8 +81,12 @@ impl Persister { rs.claim_fees_sat, rs.claim_tx_id, rs.created_at, - rs.state - FROM receive_swaps AS rs + rs.state, + sd.is_local, + sd.revision, + sd.record_id + FROM receive_swaps AS rs, sync_details sd + WHERE rs.id = sd.data_identifier {where_clause_str} ORDER BY rs.created_at " @@ -120,6 +127,11 @@ impl Persister { claim_tx_id: row.get(10)?, created_at: row.get(11)?, state: row.get(12)?, + sync_details: SyncDetails { + is_local: row.get(13)?, + revision: row.get(14)?, + record_id: row.get(15)?, + }, }) } diff --git a/lib/core/src/persist/send.rs b/lib/core/src/persist/send.rs index eca2c4336..98205d058 100644 --- a/lib/core/src/persist/send.rs +++ b/lib/core/src/persist/send.rs @@ -10,6 +10,7 @@ use crate::ensure_sdk; use crate::error::PaymentError; use crate::model::*; use crate::persist::{get_where_clause_state_in, Persister}; +use crate::sync::model::SyncDetails; impl Persister { pub(crate) fn insert_send_swap(&self, send_swap: &SendSwap) -> Result<()> { @@ -17,10 +18,11 @@ impl Persister { let mut stmt = con.prepare( " - INSERT INTO send_swaps ( + INSERT OR REPLACE INTO send_swaps ( id, id_hash, invoice, + preimage, payment_hash, description, payer_amount_sat, @@ -32,13 +34,14 @@ impl Persister { created_at, state ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", )?; 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.preimage, &send_swap.payment_hash, &send_swap.description, &send_swap.payer_amount_sat, @@ -51,6 +54,8 @@ impl Persister { &send_swap.state, ))?; + self.insert_or_update_sync_details(&send_swap.id, &send_swap.sync_details)?; + Ok(()) } @@ -80,27 +85,31 @@ impl Persister { fn list_send_swaps_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 = String::from(" AND "); where_clause_str.push_str(where_clauses.join(" AND ").as_str()); } format!( " SELECT - id, - invoice, - payment_hash, - description, - preimage, - payer_amount_sat, - receiver_amount_sat, - create_response_json, - refund_private_key, - lockup_tx_id, - refund_tx_id, - created_at, - state - FROM send_swaps + ss.id, + ss.invoice, + ss.payment_hash, + ss.description, + ss.preimage, + ss.payer_amount_sat, + ss.receiver_amount_sat, + ss.create_response_json, + ss.refund_private_key, + ss.lockup_tx_id, + ss.refund_tx_id, + ss.created_at, + ss.state, + sd.is_local, + sd.revision, + sd.record_id + FROM send_swaps ss, sync_details sd + WHERE ss.id = sd.data_identifier {where_clause_str} ORDER BY created_at " @@ -138,6 +147,11 @@ impl Persister { refund_tx_id: row.get(10)?, created_at: row.get(11)?, state: row.get(12)?, + sync_details: SyncDetails { + is_local: row.get(13)?, + revision: row.get(14)?, + record_id: row.get(15)?, + }, }) } diff --git a/lib/core/src/persist/sync.rs b/lib/core/src/persist/sync.rs new file mode 100644 index 000000000..28a64b34a --- /dev/null +++ b/lib/core/src/persist/sync.rs @@ -0,0 +1,163 @@ +use anyhow::Result; +use rusqlite::{named_params, params, Row}; + +use crate::sync::model::{sync::Record, SyncDetails}; + +use super::Persister; + +impl Persister { + pub(crate) fn get_latest_revision(&self) -> Result { + let con = self.get_connection()?; + + let latest_revision: i64 = con + .query_row( + " + SELECT revision + FROM sync_details + ORDER BY revision DESC + LIMIT 1 + ", + [], + |row| row.get(0), + ) + .unwrap_or(0); + + Ok(latest_revision) + } + + fn sql_row_to_record(&self, row: &Row) -> rusqlite::Result { + Ok(Record { + id: row.get(0)?, + schema_version: row.get(1)?, + data: row.get(2)?, + revision: row.get(3)?, + }) + } + + fn get_records_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 + id, + schema_version, + data, + revision + FROM pending_sync_records + {where_clause_str} + ORDER BY revision + " + ) + } + + pub(crate) fn get_pending_records(&self) -> Result> { + let con = self.get_connection()?; + + let where_clauses = vec![]; + let query = Self::get_records_query(where_clauses); + + let records: Vec = con + .prepare(&query)? + .query_map([], |row| self.sql_row_to_record(row))? + .map(|i| i.unwrap()) + .collect(); + + Ok(records) + } + + pub(crate) fn insert_pending_record(&self, record: &Record) -> Result<()> { + let con = self.get_connection()?; + + con.execute( + " + INSERT INTO pending_sync_records( + id, + schema_version, + data, + revision + ) + VALUES (?, ?, ?, ?) + ", + ( + &record.id, + &record.schema_version, + &record.data, + &record.revision, + ), + )?; + + Ok(()) + } + + pub(crate) fn delete_pending_record(&self, id: String) -> Result<()> { + let con = self.get_connection()?; + + con.execute( + " + DELETE FROM pending_sync_records + WHERE id = ? + ", + params![id], + )?; + + Ok(()) + } + + pub(crate) fn insert_or_update_sync_details( + &self, + id: &str, + sync_details: &SyncDetails, + ) -> Result<()> { + let con = self.get_connection()?; + + con.execute( + "INSERT OR REPLACE INTO sync_details( + data_identifier, + is_local, + revision, + record_id + ) + VALUES (:data_identifier, :is_local, :revision, :record_id)", + named_params! { + ":data_identifier": id, + ":is_local": sync_details.is_local, + ":revision": sync_details.revision, + ":record_id": sync_details.record_id + }, + )?; + + Ok(()) + } + + pub(crate) fn get_sync_details(&self, data_identifier: &str) -> Result { + let con = self.get_connection()?; + + let sync_details = con.query_row( + " + SELECT + is_local, + revision, + record_id + FROM sync_details + WHERE data_identifier = :data_identifier + ", + named_params! { + ":data_identifier": data_identifier, + }, + |row| { + Ok(SyncDetails { + is_local: row.get(0)?, + revision: row.get(1)?, + record_id: row.get(2)?, + }) + }, + )?; + + Ok(sync_details) + } +} diff --git a/lib/core/src/receive_swap.rs b/lib/core/src/receive_swap.rs index 7d659568e..5727d2104 100644 --- a/lib/core/src/receive_swap.rs +++ b/lib/core/src/receive_swap.rs @@ -61,28 +61,55 @@ 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 swap_state = RevSwapStates::from_str(&update.status).map_err(|_| { + anyhow!( + "Invalid RevSwapState for Receive Swap {id}: {}", + update.status + ) + })?; let receive_swap = self .persister .fetch_receive_swap_by_id(id)? .ok_or(anyhow!("No ongoing Receive Swap found for ID {id}"))?; - info!("Handling Receive Swap transition to {swap_state:?} for swap {id}"); - - match RevSwapStates::from_str(swap_state) { - Ok( + // The following block deals with non-local (synced) receive swaps, which require some + // pre-processing steps before continuing with the flow + if !receive_swap.sync_details.is_local { + match swap_state { + // If the state is failed, we simply mark the payment as Failed like below RevSwapStates::SwapExpired | RevSwapStates::InvoiceExpired | RevSwapStates::TransactionFailed - | RevSwapStates::TransactionRefunded, - ) => { + | RevSwapStates::TransactionRefunded => {} + + // If the state is `TransactionMempool` or `TransactionConfirmed`, we need to first + // try recovering the claim_tx_id. If no claim_tx_id is present, we continue below. + // Note: The lockup_tx_id (from Boltz's side) is already verified below. + RevSwapStates::TransactionMempool | RevSwapStates::TransactionConfirmed => { + // TODO Add claim_tx_id recovery flow + } + + // For everything else, simply show the debug message below + _ => {} + } + return Ok(()); + } + + info!("Handling Receive Swap transition to {swap_state:?} for swap {id}"); + + match swap_state { + RevSwapStates::SwapExpired + | RevSwapStates::InvoiceExpired + | RevSwapStates::TransactionFailed + | RevSwapStates::TransactionRefunded => { error!("Swap {id} entered into an unrecoverable state: {swap_state:?}"); self.update_swap_info(id, Failed, None, None).await?; 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")); }; @@ -158,7 +185,8 @@ impl ReceiveSwapHandler { Ok(()) } - Ok(RevSwapStates::TransactionConfirmed) => { + + RevSwapStates::TransactionConfirmed => { let Some(transaction) = update.transaction.clone() else { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; @@ -197,14 +225,10 @@ impl ReceiveSwapHandler { Ok(()) } - Ok(_) => { - debug!("Unhandled state for Receive Swap {id}: {swap_state}"); + _ => { + debug!("Unhandled state for Receive Swap {id}: {swap_state:?}"); Ok(()) } - - _ => Err(anyhow!( - "Invalid RevSwapState for Receive Swap {id}: {swap_state}" - )), } } diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index cec87701e..759b73a01 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -36,6 +36,7 @@ use crate::model::Signer; use crate::receive_swap::ReceiveSwapHandler; use crate::send_swap::SendSwapHandler; use crate::swapper::{boltz::BoltzSwapper, Swapper, SwapperReconnectHandler, SwapperStatusStream}; +use crate::sync::model::{SyncData, SyncDetails}; use crate::wallet::{LiquidOnchainWallet, OnchainWallet}; use crate::{ error::{PaymentError, SdkResult}, @@ -45,6 +46,8 @@ use crate::{ utils, *, }; +use self::sync::{client::BreezSyncerClient, 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; @@ -69,6 +72,7 @@ pub struct LiquidSdk { pub(crate) receive_swap_handler: ReceiveSwapHandler, pub(crate) chain_swap_handler: Arc, pub(crate) buy_bitcoin_service: Arc, + pub(crate) sync_service: Arc, } impl LiquidSdk { @@ -173,12 +177,21 @@ impl LiquidSdk { let bitcoin_chain_service = Arc::new(Mutex::new(HybridBitcoinChainService::new(config.clone())?)); + let syncer_client = Box::new(BreezSyncerClient::new()); + let sync_service = SyncService::new( + config.sync_service_url.clone(), + persister.clone(), + signer.clone(), + syncer_client, + ); + let send_swap_handler = SendSwapHandler::new( config.clone(), onchain_wallet.clone(), persister.clone(), swapper.clone(), liquid_chain_service.clone(), + sync_service.clone(), ); let receive_swap_handler = ReceiveSwapHandler::new( @@ -221,6 +234,7 @@ impl LiquidSdk { receive_swap_handler, chain_swap_handler, buy_bitcoin_service, + sync_service, }); Ok(sdk) } @@ -264,6 +278,11 @@ impl LiquidSdk { } }); + self.sync_service + .clone() + .connect(self.shutdown_receiver.clone()) + .await?; + let reconnect_handler = Box::new(SwapperReconnectHandler::new( self.persister.clone(), self.status_stream.clone(), @@ -1065,8 +1084,16 @@ impl LiquidSdk { created_at: utils::now(), state: PaymentState::Created, refund_private_key: keypair.display_secret().to_string(), + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + }, }; self.persister.insert_send_swap(&swap)?; + self.sync_service + .set_record(SyncData::Send(swap.clone().into()), None) + .await?; swap } }; @@ -1339,9 +1366,17 @@ impl LiquidSdk { refund_tx_id: None, created_at: utils::now(), state: PaymentState::Created, + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + }, }; self.persister.insert_chain_swap(&swap)?; self.status_stream.track_swap_id(&swap.id)?; + self.sync_service + .set_record(SyncData::Chain(swap.clone().into()), None) + .await?; self.wait_for_payment(Swap::Chain(swap), accept_zero_conf) .await @@ -1645,25 +1680,36 @@ impl LiquidSdk { Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()), Bolt11InvoiceDescription::Hash(_) => None, }; + + let receive_swap = ReceiveSwap { + id: swap_id.clone(), + preimage: preimage_str, + create_response_json, + claim_private_key: keypair.display_secret().to_string(), + invoice: invoice.to_string(), + payment_hash: Some(preimage_hash), + description: invoice_description, + payer_amount_sat, + receiver_amount_sat, + claim_fees_sat: reverse_pair.fees.claim_estimate(), + claim_tx_id: None, + created_at: utils::now(), + state: PaymentState::Created, + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + }, + }; self.persister - .insert_receive_swap(&ReceiveSwap { - id: swap_id.clone(), - preimage: preimage_str, - create_response_json, - claim_private_key: keypair.display_secret().to_string(), - invoice: invoice.to_string(), - payment_hash: Some(preimage_hash), - description: invoice_description, - payer_amount_sat, - receiver_amount_sat, - claim_fees_sat: reverse_pair.fees.claim_estimate(), - claim_tx_id: None, - created_at: utils::now(), - state: PaymentState::Created, - }) + .insert_receive_swap(&receive_swap) .map_err(|_| PaymentError::PersistError)?; self.status_stream.track_swap_id(&swap_id)?; + self.sync_service + .set_record(SyncData::Receive(receive_swap.into()), None) + .await?; + Ok(ReceivePaymentResponse { destination: invoice.to_string(), }) @@ -1747,9 +1793,17 @@ impl LiquidSdk { refund_tx_id: None, created_at: utils::now(), state: PaymentState::Created, + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + }, }; self.persister.insert_chain_swap(&swap)?; self.status_stream.track_swap_id(&swap.id)?; + self.sync_service + .set_record(SyncData::Chain(swap.clone().into()), None) + .await?; Ok(swap) } diff --git a/lib/core/src/send_swap.rs b/lib/core/src/send_swap.rs index 94cc28d9b..cc36851a8 100644 --- a/lib/core/src/send_swap.rs +++ b/lib/core/src/send_swap.rs @@ -17,6 +17,7 @@ use crate::chain::liquid::LiquidChainService; use crate::model::{Config, PaymentState::*, SendSwap}; use crate::prelude::Swap; use crate::swapper::Swapper; +use crate::sync::SyncService; use crate::wallet::OnchainWallet; use crate::{ensure_sdk, utils}; use crate::{ @@ -33,6 +34,7 @@ pub(crate) struct SendSwapHandler { swapper: Arc, chain_service: Arc>, subscription_notifier: broadcast::Sender, + sync_service: Arc, } impl SendSwapHandler { @@ -42,6 +44,7 @@ impl SendSwapHandler { persister: Arc, swapper: Arc, chain_service: Arc>, + sync_service: Arc, ) -> Self { let (subscription_notifier, _) = broadcast::channel::(30); Self { @@ -51,6 +54,7 @@ impl SendSwapHandler { swapper, chain_service, subscription_notifier, + sync_service, } } @@ -61,18 +65,52 @@ 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_state = SubSwapStates::from_str(&update.status) + .map_err(|_| anyhow!("Invalid SubSwapState for Send Swap {id}: {}", update.status))?; let swap = self .persister .fetch_send_swap_by_id(id)? .ok_or(anyhow!("No ongoing Send Swap found for ID {id}"))?; + // The following block deals with non-local (synced) send swaps, which require some + // pre-processing steps before continuing with the flow + if !swap.sync_details.is_local { + match swap_state { + // If the state is `InvoiceSet`, we do not lockup twice + SubSwapStates::InvoiceSet => { + debug!( + "Received state {swap_state:?} for non-local Send swap {id}. Skipping..." + ); + return Ok(()); + } + + // If the state is `TransactionClaimPending`, we try to cooperate from both + // instances + SubSwapStates::TransactionClaimPending => {} + + // If the state is `TransactionClaimed`, we recover the preimage from the script claim path + // (see below) and resolve the payment as Complete + SubSwapStates::TransactionClaimed => {} + + // If the state is failed, we have to recover both lockup and refund tx id, then + // continue with the flow below + SubSwapStates::TransactionLockupFailed + | SubSwapStates::InvoiceFailedToPay + | SubSwapStates::SwapExpired => { + // TODO Add lockup_tx_id and refund_tx_id recovery flow + } + + // For everything else, simply show the debug message below + _ => {} + } + } + 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, we proceed with locking up the funds - Ok(SubSwapStates::InvoiceSet) => { + SubSwapStates::InvoiceSet => { match (swap.state, swap.lockup_tx_id.clone()) { (PaymentState::Created, None) | (PaymentState::TimedOut, None) => { let create_response = swap.get_boltz_create_response()?; @@ -102,7 +140,9 @@ impl SendSwapHandler { warn!("Lockup tx for Send Swap {id} was already broadcast: txid {lockup_tx_id}") } (state, _) => { - debug!("Send Swap {id} is in an invalid state for {swap_state}: {state:?}") + debug!( + "Send Swap {id} is in an invalid state for {swap_state:?}: {state:?}" + ) } } Ok(()) @@ -110,7 +150,7 @@ impl SendSwapHandler { // 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:?}") @@ -120,7 +160,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 { @@ -149,11 +189,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!( @@ -192,14 +230,10 @@ impl SendSwapHandler { 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}" - )), } } diff --git a/lib/core/src/signer.rs b/lib/core/src/signer.rs index cbb2a622a..9f937cc38 100644 --- a/lib/core/src/signer.rs +++ b/lib/core/src/signer.rs @@ -19,6 +19,7 @@ use lwk_wollet::elements_miniscript::{ elementssig_to_rawsig, psbt::PsbtExt, slip77::MasterBlindingKey, + ToPublicKey as _, }; use lwk_wollet::hashes::{sha256, HashEngine, Hmac, HmacEngine}; use lwk_wollet::secp256k1::ecdsa::Signature; @@ -190,7 +191,7 @@ impl SdkSigner { }) } - fn seed(&self) -> [u8; 64] { + pub(crate) fn seed(&self) -> [u8; 64] { self.mnemonic.to_seed("") } } @@ -253,6 +254,19 @@ impl Signer for SdkSigner { .as_byte_array() .to_vec()) } + + fn ecies_encrypt(&self, msg: &[u8]) -> Result, SignerError> { + let keypair = self.xprv.to_keypair(&self.secp); + let rc_pub = keypair.public_key().to_public_key().to_bytes(); + Ok(ecies::encrypt(&rc_pub, msg) + .map_err(|err| anyhow::anyhow!("Could not encrypt data: {err}"))?) + } + + fn ecies_decrypt(&self, msg: &[u8]) -> Result, SignerError> { + let rc_prv = self.xprv.to_priv().to_bytes(); + Ok(ecies::decrypt(&rc_prv, msg) + .map_err(|err| anyhow::anyhow!("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..7293bb3c6 --- /dev/null +++ b/lib/core/src/sync/client.rs @@ -0,0 +1,73 @@ +use anyhow::{anyhow, Result}; + +use async_trait::async_trait; +use log::debug; +use tokio::sync::Mutex; + +use super::model::sync::{ + syncer_client::SyncerClient as ProtoSyncerClient, ListChangesReply, ListChangesRequest, Record, + SetRecordReply, SetRecordRequest, TrackChangesRequest, +}; + +#[async_trait] +pub(crate) trait SyncerClient: Send + Sync { + async fn connect(&self, connect_url: String) -> Result<()>; + async fn set_record(&self, req: SetRecordRequest) -> Result; + async fn list_changes(&self, req: ListChangesRequest) -> Result; + async fn track_changes( + &self, + req: TrackChangesRequest, + ) -> anyhow::Result>; + async fn disconnect(&self) -> Result<()>; +} + +pub(crate) struct BreezSyncerClient { + inner: Mutex>>, +} + +impl BreezSyncerClient { + pub(crate) fn new() -> Self { + Self { + inner: Default::default(), + } + } +} + +#[async_trait] +impl SyncerClient for BreezSyncerClient { + async fn connect(&self, connect_url: String) -> Result<()> { + let mut client = self.inner.lock().await; + *client = Some(ProtoSyncerClient::connect(connect_url.clone()).await?); + debug!("Successfully connected to {connect_url}"); + Ok(()) + } + + async fn set_record(&self, req: SetRecordRequest) -> Result { + let Some(mut client) = self.inner.lock().await.clone() else { + return Err(anyhow!("Cannot run `set_record`: client not connected")); + }; + Ok(client.set_record(req).await?.into_inner()) + } + async fn list_changes(&self, req: ListChangesRequest) -> Result { + let Some(mut client) = self.inner.lock().await.clone() else { + return Err(anyhow!("Cannot run `list_changes`: client not connected")); + }; + Ok(client.list_changes(req).await?.into_inner()) + } + + async fn track_changes( + &self, + req: TrackChangesRequest, + ) -> Result> { + let Some(mut client) = self.inner.lock().await.clone() else { + return Err(anyhow!("Cannot run `listen_changes`: client not connected")); + }; + Ok(client.track_changes(req).await?.into_inner()) + } + + async fn disconnect(&self) -> Result<()> { + let mut client = self.inner.lock().await; + *client = None; + Ok(()) + } +} diff --git a/lib/core/src/sync/mod.rs b/lib/core/src/sync/mod.rs new file mode 100644 index 000000000..50a28fe86 --- /dev/null +++ b/lib/core/src/sync/mod.rs @@ -0,0 +1,350 @@ +pub(crate) mod client; +pub(crate) mod model; + +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use futures_util::TryFutureExt; +use log::{debug, warn}; +use std::collections::HashMap; +use tokio::sync::{watch, Mutex}; +use tokio_stream::StreamExt as _; + +use self::client::SyncerClient; +use self::model::sync::TrackChangesRequest; +use self::model::{ + sync::{ListChangesRequest, Record, SetRecordReply, SetRecordRequest, SetRecordStatus}, + DecryptedRecord, SyncData, +}; +use self::model::{Merge, OutboundChange, SyncDetails}; +use crate::prelude::{ChainSwap, ReceiveSwap, SendSwap}; +use crate::{model::Signer, persist::Persister, utils}; + +const CURRENT_SCHEMA_VERSION: f32 = 0.01; +const MAX_SET_RECORD_REATTEMPTS: u8 = 5; + +pub(crate) struct SyncService { + connect_url: String, + persister: Arc, + signer: Arc>, + client: Box, + outbound_changes: Mutex>, +} + +impl SyncService { + pub(crate) fn new( + connect_url: String, + persister: Arc, + signer: Arc>, + client: Box, + ) -> Arc { + Arc::new(Self { + connect_url, + persister, + signer, + client, + outbound_changes: Default::default(), + }) + } + + /// Connects to the gRPC endpoint specified in [SyncService::connect_url] and starts listening + /// for changes + /// Additionally, this method pulls the latest changes from the remote and applies them + pub(crate) async fn connect( + self: Arc, + shutdown_receiver: watch::Receiver<()>, + ) -> Result<()> { + self.client.connect(self.connect_url.clone()).await?; + self.sync_with_tip().await?; + self.listen(shutdown_receiver).await?; + Ok(()) + } + + /// Listens to updates for new changes. + /// This method ignores changes we broadcasted by referring to the `sent_counter` inner hashset + /// Records which are received from an external instance are instantly applied to the local + /// database. Errors are skipped. + async fn listen(self: Arc, mut shutdown_receiver: watch::Receiver<()>) -> Result<()> { + let request = TrackChangesRequest::new(utils::now(), self.signer.clone()) + .map_err(|err| anyhow!("Could not sign ListenChangesRequest: {err:?}"))?; + + let cloned = self.clone(); + tokio::spawn(async move { + let mut stream = match cloned.client.track_changes(request).await { + Ok(stream) => stream, + Err(err) => return warn!("Could not listen to changes: {err:?}"), + }; + + debug!("Started listening to changes"); + + loop { + tokio::select! { + Some(message) = stream.next() => { + match message { + Ok(record) => { + let mut sent = cloned.outbound_changes.lock().await; + if sent.get(&record.id).is_some() { + sent.remove(&record.id); + continue; + } + + let record_id = record.id.clone(); + let record_revision = record.revision; + let record_schema_version = record.schema_version; + + debug!( + "Received new record - record_id {record_id} record_revision {record_revision} record_schema_version {record_schema_version}" + ); + + if let Err(err) = cloned.apply_changes(&[record]).await { + warn!("Could not apply incoming changes: {err:?}") + }; + + debug!("Successfully applied incoming changes for record {record_id}",) + } + Err(err) => warn!("An error occured while listening for records: {err:?}"), + } + } + _ = shutdown_receiver.changed() => { + debug!("Received shutdown signal, exiting sync loop"); + if let Err(err) = cloned.client.disconnect().await { + debug!("Could not disconnect sync client: {err:?}"); + } + return; + } + } + } + }); + + Ok(()) + } + + async fn apply_record(&self, record: DecryptedRecord) -> Result<()> { + debug!( + "Applying record - id {} revision {}", + &record.id, record.revision + ); + + match record.data { + SyncData::Chain(chain_data) => self.persister.insert_chain_swap( + &ChainSwap::from_sync_data(chain_data, record.revision, record.id), + ), + SyncData::Send(send_data) => self.persister.insert_send_swap( + &SendSwap::from_sync_data(send_data, record.revision, record.id), + ), + SyncData::Receive(receive_data) => { + self.persister + .insert_receive_swap(&ReceiveSwap::from_sync_data( + receive_data, + record.revision, + record.id, + )) + } + } + } + + /// Collects the given records in two categories: upgradable and non-upgradable, based on their + /// schema_version + fn collect_records<'a>( + &self, + records: &'a [Record], + ) -> (Vec, Vec<&'a Record>) { + let mut failed_records = vec![]; + let mut updatable_records = vec![]; + + for record in records { + // If it's a major version ahead, we skip + if record.schema_version.floor() > CURRENT_SCHEMA_VERSION.floor() { + failed_records.push(record); + continue; + } + + let decrypted_record = + match DecryptedRecord::try_from_record(self.signer.clone(), record) { + Ok(record) => record, + Err(err) => { + warn!("Could not decrypt record: {err:?}"); + continue; + } + }; + + updatable_records.push(decrypted_record) + } + + (updatable_records, failed_records) + } + + /// Applies a given set of changes into the local database + /// For each record, if its (schema_version)[Record::schema_version] is greater than the + /// [CURRENT_SCHEMA_VERSION], the record will be persisted into the `pending_sync_records` and + /// applied later. + /// Instead, if the schema_version is less or equal to the client's current version, it will try + /// and apply the changes, skipping errors if any + pub(crate) async fn apply_changes(&self, records: &[Record]) -> Result<()> { + let (updatable_records, failed_records) = self.collect_records(records); + + // We persist records which we cannot update (> CURRENT_SCHEMA_VERSION) + for record in failed_records { + self.persister.insert_pending_record(record)?; + } + + let changes = self.outbound_changes.lock().await; + + // We apply records which we can update (<= CURRENT_SCHEMA_VERSION) + // Additionally, if there is a conflict with an outbound record, it will be resolved + // before persisting the update + for mut record in updatable_records { + if let Some(outgoing_record) = changes.get(&record.id) { + if let Err(err) = record + .data + .merge(&outgoing_record.data, &outgoing_record.updated_fields) + { + warn!("Could not merge inbound record with outbound changes: {err:?}"); + continue; + } + } + + if let Err(err) = self.apply_record(record).await { + warn!("Could not apply record changes: {err:?}"); + } + } + + Ok(()) + } + + async fn get_changes_since(&self, from_id: i64) -> Result> { + let request = ListChangesRequest::new(from_id, utils::now(), self.signer.clone()) + .map_err(|err| anyhow!("Could not sign ListChangesRequest: {err:?}"))?; + let records = self.client.list_changes(request).await?.changes; + Ok(records) + } + + /// Pulls the latest changes from the remote, *without* updating the local database + pub(crate) async fn get_latest_changes(&self) -> Result> { + let latest_revision = self.persister.get_latest_revision()?; + let records = self.get_changes_since(latest_revision).await?; + Ok(records) + } + + async fn sync_with_tip(&self) -> Result<()> { + let records = self.get_latest_changes().await?; + self.apply_changes(&records).await + } + + /// Syncs the given data outwards + /// If `updated_fields` is specified, the method will look into the local `sync_details` cache and + /// get the correct revision number and record id, as well as calculate the number of + /// set_record attempts which have been executed so far + pub(crate) async fn set_record( + &self, + data: SyncData, + updated_fields: Option<&[&'static str]>, + ) -> Result<()> { + let data_identifier = data.id(); + let (record_id, revision, attempts) = match updated_fields { + Some(fields) => { + let existing_sync_details = self.persister.get_sync_details(&data_identifier)?; + + let record_id = existing_sync_details.record_id.ok_or(anyhow!( + "Expecting valid record_id when calling `set_record` with updated fields", + ))?; + let revision = existing_sync_details.revision.ok_or(anyhow!( + "Expecting valid revision when calling `set_record` with updated fields", + ))?; + + let mut changes = self.outbound_changes.lock().await; + let attempts = match changes.get_mut(&record_id) { + Some(change) => { + change.increment_counter(); + change.reattempt_counter + } + None => { + let change = OutboundChange::new(data.clone(), fields.into()); + let reattempt_counter = change.reattempt_counter; + changes.insert(record_id.clone(), change); + reattempt_counter + } + }; + + (record_id, revision, Some(attempts)) + } + None => (uuid::Uuid::new_v4().to_string(), 0, None), + }; + let record = Record::new( + record_id.clone(), + data.clone(), + revision, + self.signer.clone(), + )?; + + debug!("Starting outward sync (set_record) for record {record_id} updated_fields {updated_fields:?}"); + + let request = SetRecordRequest::new(record, utils::now(), self.signer.clone()) + .map_err(|err| anyhow!("Could not sign SetRecordRequest: {err:?}"))?; + + match self.client.set_record(request).await { + Ok(SetRecordReply { + status, + new_revision, + }) => { + if SetRecordStatus::try_from(status)? == SetRecordStatus::Conflict { + if let Some(attempts) = attempts { + if attempts > MAX_SET_RECORD_REATTEMPTS { + return Err(anyhow!( + "Could not set record: revision conflict and max reattempts reached." + )); + } + + return Box::pin( + self.sync_with_tip() + .and_then(|_| self.set_record(data, updated_fields)), + ) + .await; + } + + // Impossible scenario - we cannot get a conflict on a newly created record + return Err(anyhow!("Could not set record: conflict detected by the server on newly created record.")); + } + + self.persister.insert_or_update_sync_details( + &data_identifier, + &SyncDetails { + is_local: true, + revision: Some(new_revision), + record_id: Some(record_id.clone()), + }, + )?; + + debug!("Successfully synced (set_record) id {record_id} updated_fields {updated_fields:?} revision {new_revision}"); + Ok(()) + } + Err(err) => { + self.outbound_changes.lock().await.remove(&record_id); + debug!("Could not sync record (set_record) id {record_id} updated_fields {updated_fields:?}: {err:?}"); + Err(err) + } + } + } + + /// Cleans up the cached pending records, by trying to apply each one + /// This method is especially useful once a client has upgraded, and is now able to + /// successfully apply a record with a higher schema_version (see [SyncService::apply_changes]) + pub(crate) async fn cleanup(&self) -> Result<()> { + let pending_records = self + .persister + .get_pending_records() + .map_err(|err| anyhow!("Could not fetch pending records from database: {err:?}"))?; + + let (updatable_records, _) = self.collect_records(&pending_records); + + for record in updatable_records { + let record_id = record.id.clone(); + if self.apply_record(record).await.is_err() { + continue; + } + self.persister.delete_pending_record(record_id)?; + } + + Ok(()) + } +} diff --git a/lib/core/src/sync/model/mod.rs b/lib/core/src/sync/model/mod.rs new file mode 100644 index 000000000..2352166a9 --- /dev/null +++ b/lib/core/src/sync/model/mod.rs @@ -0,0 +1,451 @@ +use std::sync::Arc; + +use lwk_wollet::hashes::hex::DisplayHex; +use openssl::sha::sha256; +use serde::{Deserialize, Serialize}; + +use self::sync::{ListChangesRequest, Record, SetRecordRequest, TrackChangesRequest}; +use crate::model::{ + ChainSwap, Direction, PaymentState, ReceiveSwap, SendSwap, Signer, SignerError, +}; + +use super::CURRENT_SCHEMA_VERSION; + +pub(crate) mod sync; + +const MESSAGE_PREFIX: &[u8; 13] = b"realtimesync:"; + +/// Denotes the remote details of an object, more specifically whether or not it originates from a +/// sync and what the remote id of the record is (if any) +#[derive(Clone, Debug)] +pub(crate) struct SyncDetails { + pub(crate) is_local: bool, + pub(crate) revision: Option, + pub(crate) record_id: Option, +} + +pub(crate) trait Merge { + fn merge(&mut self, other: &Self, updated_fields: &[&'static str]) + -> Result<(), anyhow::Error>; +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct ChainSyncData { + pub(crate) swap_id: String, + pub(crate) preimage: String, + pub(crate) create_response_json: String, + pub(crate) direction: Direction, + pub(crate) lockup_address: String, + pub(crate) claim_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 From for ChainSyncData { + fn from(value: ChainSwap) -> Self { + Self { + swap_id: value.id, + preimage: value.preimage, + create_response_json: value.create_response_json, + direction: value.direction, + lockup_address: value.lockup_address, + claim_address: value.claim_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 Merge for ChainSyncData { + fn merge( + &mut self, + other: &Self, + updated_fields: &[&'static str], + ) -> Result<(), anyhow::Error> { + for field in updated_fields { + match *field { + "accept_zero_conf" => self.accept_zero_conf = other.accept_zero_conf, + "preimage" => self.preimage.clone_from(&other.preimage), + "description" => self.description.clone_from(&other.description), + _ => { + return Err(anyhow::anyhow!("Unsupported merge field")); + } + }; + } + + Ok(()) + } +} + +impl ChainSwap { + pub(crate) fn from_sync_data(val: ChainSyncData, revision: i64, record_id: String) -> Self { + ChainSwap { + id: val.swap_id, + direction: val.direction, + 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, + accept_zero_conf: val.accept_zero_conf, + created_at: val.created_at, + lockup_address: val.lockup_address, + claim_address: val.claim_address, + claim_fees_sat: val.claim_fees_sat, + claim_private_key: val.claim_private_key, + refund_private_key: val.refund_private_key, + create_response_json: val.create_response_json, + server_lockup_tx_id: None, + user_lockup_tx_id: None, + claim_tx_id: None, + refund_tx_id: None, + state: PaymentState::Created, + sync_details: SyncDetails { + is_local: false, + revision: Some(revision), + record_id: Some(record_id), + }, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct SendSyncData { + pub(crate) swap_id: String, + pub(crate) invoice: 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) payment_hash: Option, + pub(crate) description: Option, +} + +impl From for SendSyncData { + fn from(value: SendSwap) -> Self { + Self { + swap_id: value.id, + payment_hash: value.payment_hash, + invoice: value.invoice, + 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, + } + } +} + +impl Merge for SendSyncData { + fn merge( + &mut self, + other: &Self, + updated_fields: &[&'static str], + ) -> Result<(), anyhow::Error> { + for field in updated_fields { + match *field { + "payment_hash" => self.payment_hash.clone_from(&other.payment_hash), + "preimage" => self.preimage.clone_from(&other.preimage), + "description" => self.description.clone_from(&other.description), + _ => { + return Err(anyhow::anyhow!("Unsupported merge field")); + } + }; + } + + Ok(()) + } +} + +impl SendSwap { + pub(crate) fn from_sync_data(val: SendSyncData, revision: i64, record_id: String) -> Self { + SendSwap { + id: val.swap_id, + payment_hash: val.payment_hash, + invoice: val.invoice, + description: val.description, + preimage: val.preimage, + payer_amount_sat: val.payer_amount_sat, + receiver_amount_sat: val.receiver_amount_sat, + create_response_json: val.create_response_json, + refund_private_key: val.refund_private_key, + created_at: val.created_at, + lockup_tx_id: None, + refund_tx_id: None, + state: PaymentState::Created, + sync_details: SyncDetails { + is_local: false, + revision: Some(revision), + record_id: Some(record_id), + }, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub(crate) struct ReceiveSyncData { + pub(crate) swap_id: String, + pub(crate) invoice: String, + pub(crate) preimage: 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) 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, + 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, + created_at: value.created_at, + description: value.description, + } + } +} + +impl Merge for ReceiveSyncData { + fn merge( + &mut self, + other: &Self, + updated_fields: &[&'static str], + ) -> Result<(), anyhow::Error> { + for field in updated_fields { + match *field { + "payment_hash" => self.payment_hash.clone_from(&other.payment_hash), + "preimage" => self.preimage.clone_from(&other.preimage), + "description" => self.description.clone_from(&other.description), + _ => { + return Err(anyhow::anyhow!("Unsupported merge field")); + } + }; + } + + Ok(()) + } +} + +impl ReceiveSwap { + pub(crate) fn from_sync_data(val: ReceiveSyncData, revision: i64, record_id: String) -> Self { + ReceiveSwap { + id: val.swap_id, + payment_hash: val.payment_hash, + preimage: val.preimage, + create_response_json: val.create_response_json, + claim_private_key: val.claim_private_key, + invoice: val.invoice, + description: val.description, + payer_amount_sat: val.payer_amount_sat, + receiver_amount_sat: val.receiver_amount_sat, + claim_fees_sat: val.claim_fees_sat, + created_at: val.created_at, + claim_tx_id: None, + state: PaymentState::Created, + sync_details: SyncDetails { + is_local: false, + revision: Some(revision), + record_id: Some(record_id), + }, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "data_type", content = "data")] +pub(crate) enum SyncData { + Chain(ChainSyncData), + Send(SendSyncData), + Receive(ReceiveSyncData), +} + +impl SyncData { + pub(crate) fn to_bytes(&self) -> serde_json::Result> { + serde_json::to_vec(self) + } + + pub(crate) fn id(&self) -> String { + match self { + SyncData::Receive(receive_sync_data) => receive_sync_data.swap_id.clone(), + SyncData::Send(send_sync_data) => send_sync_data.swap_id.clone(), + SyncData::Chain(chain_sync_data) => chain_sync_data.swap_id.clone(), + } + } +} + +impl Merge for SyncData { + fn merge( + &mut self, + other: &Self, + updated_fields: &[&'static str], + ) -> Result<(), anyhow::Error> { + match (self, other) { + (SyncData::Receive(receive_data), SyncData::Receive(other)) => { + receive_data.merge(other, updated_fields)? + } + (SyncData::Send(send_data), SyncData::Send(other)) => { + send_data.merge(other, updated_fields)? + } + (SyncData::Chain(chain_data), SyncData::Chain(other)) => { + chain_data.merge(other, updated_fields)? + } + _ => { + return Err(anyhow::anyhow!("Cannot merge sync data: type mismatch")); + } + }; + + Ok(()) + } +} + +pub(crate) struct DecryptedRecord { + pub(crate) id: String, + pub(crate) revision: i64, + pub(crate) schema_version: f32, + pub(crate) data: SyncData, +} + +impl DecryptedRecord { + pub(crate) fn try_from_record( + signer: Arc>, + record: &Record, + ) -> anyhow::Result { + let dec_data = signer.ecies_decrypt(record.data.as_slice())?; + let data = serde_json::from_slice(&dec_data)?; + Ok(Self { + id: record.id.clone(), + revision: record.revision, + schema_version: record.schema_version, + data, + }) + } +} + +impl Record { + pub(crate) fn new( + id: String, + data: SyncData, + revision: i64, + signer: Arc>, + ) -> Result { + let data = data.to_bytes()?; + let data = signer + .ecies_encrypt(&data) + .map_err(|err| anyhow::anyhow!("Could not encrypt sync data: {err:?}"))?; + Ok(Self { + id, + revision, + schema_version: CURRENT_SCHEMA_VERSION, + data, + }) + } +} + +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, + }) + } +} + +impl ListChangesRequest { + pub(crate) fn new( + since_revision: i64, + request_time: u32, + signer: Arc>, + ) -> Result { + let msg = format!("{}-{}", since_revision, request_time); + let signature = sign_message(msg.as_bytes(), signer)?; + Ok(Self { + since_revision, + request_time, + signature, + }) + } +} + +impl TrackChangesRequest { + pub(crate) fn new( + request_time: u32, + signer: Arc>, + ) -> Result { + let msg = format!("{}", request_time); + let signature = sign_message(msg.as_bytes(), signer)?; + Ok(Self { + request_time, + signature, + }) + } +} + +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)) +} + +pub(crate) struct OutboundChange { + pub(crate) data: SyncData, + pub(crate) reattempt_counter: u8, + pub(crate) updated_fields: Vec<&'static str>, +} + +impl OutboundChange { + pub(crate) fn new(data: SyncData, updated_fields: Vec<&'static str>) -> Self { + Self { + data, + updated_fields, + reattempt_counter: 1, + } + } + + pub(crate) fn increment_counter(&mut self) { + self.reattempt_counter += 1; + } +} diff --git a/lib/core/src/sync/model/sync.rs b/lib/core/src/sync/model/sync.rs new file mode 100644 index 000000000..407b9174c --- /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(int64, tag = "2")] + pub revision: i64, + #[prost(float, tag = "3")] + pub schema_version: f32, + #[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(int64, tag = "2")] + pub new_revision: i64, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListChangesRequest { + #[prost(int64, tag = "1")] + pub since_revision: i64, + #[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..b3826a65f --- /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; + int64 revision = 2; + float 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; + int64 new_revision = 2; +} + +message ListChangesRequest { + int64 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_swap.rs b/lib/core/src/test_utils/chain_swap.rs index 9be33669a..935c0d9be 100644 --- a/lib/core/src/test_utils/chain_swap.rs +++ b/lib/core/src/test_utils/chain_swap.rs @@ -13,6 +13,7 @@ use crate::{ model::{ChainSwap, Config, Direction, PaymentState}, persist::Persister, swapper::boltz::BoltzSwapper, + sync::model::SyncDetails, utils, }; @@ -112,6 +113,11 @@ pub(crate) fn new_chain_swap( created_at: utils::now(), state: payment_state.unwrap_or(PaymentState::Created), accept_zero_conf, + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + } }, Direction::Outgoing => ChainSwap { id: generate_random_string(4), @@ -175,6 +181,11 @@ pub(crate) fn new_chain_swap( created_at: utils::now(), state: payment_state.unwrap_or(PaymentState::Created), accept_zero_conf, + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + } } } } diff --git a/lib/core/src/test_utils/mod.rs b/lib/core/src/test_utils/mod.rs index 9e71df43e..9312f0b2a 100644 --- a/lib/core/src/test_utils/mod.rs +++ b/lib/core/src/test_utils/mod.rs @@ -10,6 +10,7 @@ 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 811781067..256eb67fb 100644 --- a/lib/core/src/test_utils/persist.rs +++ b/lib/core/src/test_utils/persist.rs @@ -15,6 +15,7 @@ use tempdir::TempDir; use crate::{ model::{LiquidNetwork, PaymentState, PaymentTxData, PaymentType, ReceiveSwap, SendSwap}, persist::Persister, + sync::model::SyncDetails, test_utils::generate_random_string, utils, }; @@ -32,7 +33,7 @@ pub(crate) fn new_send_swap(payment_state: Option) -> SendSwap { let payment_hash = sha256::Hash::from_slice(&[0; 32][..]).expect("Expecting valid hash"); let invoice = InvoiceBuilder::new(Currency::BitcoinTestnet) .description("Test invoice".into()) - .payment_hash(payment_hash.clone()) + .payment_hash(payment_hash) .payment_secret(PaymentSecret([42u8; 32])) .current_timestamp() .min_final_cltv_expiry_delta(144) @@ -71,6 +72,11 @@ pub(crate) fn new_send_swap(payment_state: Option) -> SendSwap { created_at: utils::now(), state: payment_state.unwrap_or(PaymentState::Created), refund_private_key: "945affeef55f12227f1d4a3f80a17062a05b229ddc5a01591eb5ddf882df92e3".to_string(), + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + } } } @@ -105,6 +111,11 @@ pub(crate) fn new_receive_swap(payment_state: Option) -> ReceiveSw claim_tx_id: None, created_at: utils::now(), state: payment_state.unwrap_or(PaymentState::Created), + sync_details: SyncDetails { + is_local: true, + revision: None, + record_id: None, + } } } diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index ae34026bb..6ae48c1af 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -15,12 +15,14 @@ use crate::{ receive_swap::ReceiveSwapHandler, sdk::LiquidSdk, send_swap::SendSwapHandler, + sync::SyncService, }; use super::{ chain::{MockBitcoinChainService, MockLiquidChainService}, status_stream::MockStatusStream, swapper::MockSwapper, + sync::MockSyncerClient, wallet::{MockSigner, MockWallet}, }; @@ -58,12 +60,21 @@ pub(crate) fn new_liquid_sdk_with_chain_services( let signer: Arc> = Arc::new(Box::new(MockSigner::new())); let onchain_wallet = Arc::new(MockWallet::new()); + let syncer_client = Box::new(MockSyncerClient::new()); + let sync_service = SyncService::new( + "".to_string(), + persister.clone(), + signer.clone(), + syncer_client, + ); + let send_swap_handler = SendSwapHandler::new( config.clone(), onchain_wallet.clone(), persister.clone(), swapper.clone(), liquid_chain_service.clone(), + sync_service.clone(), ); let receive_swap_handler = ReceiveSwapHandler::new( @@ -109,5 +120,6 @@ pub(crate) fn new_liquid_sdk_with_chain_services( receive_swap_handler, chain_swap_handler, buy_bitcoin_service, + sync_service, }) } diff --git a/lib/core/src/test_utils/send_swap.rs b/lib/core/src/test_utils/send_swap.rs index e2f559b9a..330d8005d 100644 --- a/lib/core/src/test_utils/send_swap.rs +++ b/lib/core/src/test_utils/send_swap.rs @@ -2,11 +2,21 @@ use std::sync::Arc; -use crate::{model::Config, persist::Persister, send_swap::SendSwapHandler}; +use crate::{ + model::{Config, Signer}, + persist::Persister, + send_swap::SendSwapHandler, + sync::SyncService, +}; use anyhow::Result; use tokio::sync::Mutex; -use super::{chain::MockLiquidChainService, swapper::MockSwapper, wallet::MockWallet}; +use super::{ + chain::MockLiquidChainService, + swapper::MockSwapper, + sync::MockSyncerClient, + wallet::{MockSigner, MockWallet}, +}; pub(crate) fn new_send_swap_handler(persister: Arc) -> Result { let config = Config::testnet(None); @@ -14,11 +24,21 @@ pub(crate) fn new_send_swap_handler(persister: Arc) -> Result> = Arc::new(Box::new(MockSigner::new())); + let syncer_client = Box::new(MockSyncerClient::new()); + let sync_service = SyncService::new( + "".to_string(), + persister.clone(), + signer.clone(), + syncer_client, + ); + Ok(SendSwapHandler::new( config, onchain_wallet, persister, swapper, chain_service, + sync_service, )) } diff --git a/lib/core/src/test_utils/sync.rs b/lib/core/src/test_utils/sync.rs new file mode 100644 index 000000000..6e00a6531 --- /dev/null +++ b/lib/core/src/test_utils/sync.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use async_trait::async_trait; + +use crate::sync::client::SyncerClient; +use crate::sync::model::sync::{ + ListChangesReply, ListChangesRequest, Record, SetRecordReply, SetRecordRequest, + TrackChangesRequest, +}; + +pub(crate) struct MockSyncerClient {} + +impl MockSyncerClient { + pub(crate) fn new() -> Self { + Self {} + } +} + +#[async_trait] +impl SyncerClient for MockSyncerClient { + async fn connect(&self, _connect_url: String) -> Result<()> { + Ok(()) + } + + async fn set_record(&self, _req: SetRecordRequest) -> Result { + unimplemented!() + } + async fn list_changes(&self, _req: ListChangesRequest) -> Result { + unimplemented!() + } + + async fn track_changes( + &self, + _req: TrackChangesRequest, + ) -> Result> { + unimplemented!() + } + + async fn disconnect(&self) -> Result<()> { + Ok(()) + } +} diff --git a/lib/core/src/test_utils/wallet.rs b/lib/core/src/test_utils/wallet.rs index e5c277e64..0da85eee5 100644 --- a/lib/core/src/test_utils/wallet.rs +++ b/lib/core/src/test_utils/wallet.rs @@ -114,4 +114,12 @@ impl Signer for MockSigner { fn hmac_sha256(&self, _msg: Vec, _derivation_path: String) -> Result, SignerError> { todo!() } + + fn ecies_encrypt(&self, _msg: &[u8]) -> Result, SignerError> { + todo!() + } + + fn ecies_decrypt(&self, _msg: &[u8]) -> Result, SignerError> { + todo!() + } } diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index a382c9dbb..e3b444239 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1646,7 +1646,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 != 9) throw Exception('unexpected arr length: expect 9 but see ${arr.length}'); + if (arr.length != 10) throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); return Config( liquidElectrumUrl: dco_decode_String(arr[0]), bitcoinElectrumUrl: dco_decode_String(arr[1]), @@ -1655,8 +1655,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: dco_decode_liquid_network(arr[4]), paymentTimeoutSec: dco_decode_u_64(arr[5]), zeroConfMinFeeRateMsat: dco_decode_u_32(arr[6]), - zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[7]), - breezApiKey: dco_decode_opt_String(arr[8]), + syncServiceUrl: dco_decode_String(arr[7]), + zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[8]), + breezApiKey: dco_decode_opt_String(arr[9]), ); } @@ -3425,6 +3426,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); return Config( @@ -3435,6 +3437,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); } @@ -5323,6 +5326,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); } diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index d5f2bf4b6..bbea1ea1d 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -1997,6 +1997,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); } @@ -5574,6 +5575,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; diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index bc2ea5cb8..bcbd77868 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -131,6 +131,9 @@ class Config { /// Zero-conf minimum accepted fee-rate in millisatoshis per vbyte final int zeroConfMinFeeRateMsat; + /// The URL of the service used to synchronize data across devices + final String syncServiceUrl; + /// Maximum amount in satoshi to accept zero-conf payments with /// Defaults to [crate::receive_swap::DEFAULT_ZERO_CONF_MAX_SAT] final BigInt? zeroConfMaxAmountSat; @@ -146,6 +149,7 @@ class Config { required this.network, required this.paymentTimeoutSec, required this.zeroConfMinFeeRateMsat, + required this.syncServiceUrl, this.zeroConfMaxAmountSat, this.breezApiKey, }); @@ -159,6 +163,7 @@ class Config { network.hashCode ^ paymentTimeoutSec.hashCode ^ zeroConfMinFeeRateMsat.hashCode ^ + syncServiceUrl.hashCode ^ zeroConfMaxAmountSat.hashCode ^ breezApiKey.hashCode; @@ -174,6 +179,7 @@ class Config { network == other.network && paymentTimeoutSec == other.paymentTimeoutSec && zeroConfMinFeeRateMsat == other.zeroConfMinFeeRateMsat && + syncServiceUrl == other.syncServiceUrl && zeroConfMaxAmountSat == other.zeroConfMaxAmountSat && breezApiKey == other.breezApiKey; }