Payment lifecycle (#184)

* Rename swap tables: remove ongoing_ prefix

* Add swap status enums and filtering

* Swap-in: add claim_txid

* Swap-out: add claim_txid

* resolve_swap: Don't remove swap when complete

* Fixups after rebase

* Remove unused method

* Consider payment as pending based on confirmations

An onchain payment with no confirmations is considered
pending. The previous logic of converting pending swaps
into pending payments is removed, since we may have
pending swaps that should not result in pending
payments (for example on Receive, before the invoice
is paid).

* Fix swap-in query

* GetInfoResponse: fix balance, include pending

* Remove unused method

* Re-generate flutter bridge files

* Re-generate RN bindings

* Fix payment_type detection in list_payments

* Send: persist to DB when claim tx is seen

* Receive: fix occasional error when broadcasting claim

* Remove fixed TODO

* Receive: only rescan on testnet, where Electrum is used to broadcast

* Log more details when broadcasting fails

* Improve AlreadyClaimed error detection and handling

* Rename SubmarineSwapStatus::Initial to Created

* Split pending payment types into separate field status

* Rename swap status enums

* Fix INSERT query

* Bump lwk libraries

* Simplify Receive try_handle_reverse_swap_status loop

* Change resolve_swap to insert_or_update_payment

* Refactor payment data persistence

* Remove unused dependency

* Bump LWK dependencies

* Rename reconcile_payments_with_onchain

* Rename try_claim_v2

* Rename address() to next_unused_address()

* Move all claim persistence writes in try_claim

* Flatten Payment struct

* Re-generate bindings

* Expose sync() in service interface

* Set Send ws stream as nonblocking, use singleton stream

* Send_payment: sync() before handling new state

* Sync() on sdk.connect()

* Remove unused args from list_payments()

* Receive: rename DB field redeem_script to response JSON

* Convert to and from internal structs to persist CreateResponse JSON

* De-duplicate internal CreateResponse structs to prevent storing same field twice

* Schedule a periodic sync() thread on startup

* Persist swap states and add methods to transition between them

* Handle unwrap() when subscribing for WS updates

* Status Stream: handle remaining unwraps() and TODOs

* Consolidate status transitions into two SDK methods

* Status Stream: reconnect and resume tracking on disconnect

* Remove superfluous TODO

* Send swaps: correctly transition to Complete even if app killed during send_payment()

* State transitions: Move SQL queries to persistence layer

* Send: handle edge TransactionClaimed edge-case

* Send: mark as Complete after we check the preimage

* Send: remove marking as Complete on TransactionClaimed
This commit is contained in:
ok300
2024-05-22 20:00:38 +00:00
committed by GitHub
parent ce24aef3c8
commit c975da5b3c
32 changed files with 2050 additions and 1036 deletions

248
cli/Cargo.lock generated
View File

@@ -189,6 +189,12 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@@ -355,10 +361,16 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bmp-monochrome"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567"
[[package]] [[package]]
name = "boltz-client" name = "boltz-client"
version = "0.1.3" version = "0.1.3"
source = "git+https://github.com/hydra-yse/boltz-rust?rev=be8395900495e415699a54e15f4bd0bc6d774237#be8395900495e415699a54e15f4bd0bc6d774237" source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-05-21#a6254147a0d00756880f5de38ac6000e49f61560"
dependencies = [ dependencies = [
"bip39", "bip39",
"bitcoin 0.31.2", "bitcoin 0.31.2",
@@ -398,7 +410,6 @@ dependencies = [
"anyhow", "anyhow",
"bip39", "bip39",
"boltz-client", "boltz-client",
"elements",
"flutter_rust_bridge", "flutter_rust_bridge",
"glob", "glob",
"log", "log",
@@ -674,9 +685,9 @@ dependencies = [
[[package]] [[package]]
name = "elements-miniscript" name = "elements-miniscript"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73842aeed05c6d62a985672f651914080e6f1cedced5ea194405f5b5f838f6dd" checksum = "cd2c9c3a0acd5a30061dc5f1859b816ef01d320da66d074d9e23aad40e7a7eba"
dependencies = [ dependencies = [
"bitcoin 0.31.2", "bitcoin 0.31.2",
"elements", "elements",
@@ -964,6 +975,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -1001,16 +1021,16 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.26" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
dependencies = [ dependencies = [
"atomic-waker",
"bytes", "bytes",
"fnv", "fnv",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "http",
"http 0.2.12",
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
@@ -1091,17 +1111,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.1.0" version = "1.1.0"
@@ -1115,12 +1124,24 @@ dependencies = [
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.6" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [ dependencies = [
"bytes", "bytes",
"http 0.2.12", "http",
]
[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1130,12 +1151,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "1.3.0" version = "1.3.0"
@@ -1153,39 +1168,59 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.28" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http 0.2.12", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2", "smallvec",
"tokio", "tokio",
"tower-service",
"tracing",
"want", "want",
] ]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-rustls"
version = "0.5.0" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http",
"hyper",
"hyper-util",
"rustls 0.22.4",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper", "hyper",
"native-tls", "pin-project-lite",
"socket2",
"tokio", "tokio",
"tokio-native-tls", "tower",
"tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -1347,22 +1382,24 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "lwk_common" name = "lwk_common"
version = "0.3.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93bd7176520b15e692ee7d778f8f9fffe39a26b5df994d8f0e4295389576343" checksum = "451ed06b50b1bb76d6a910c36039a687f891868bd199c110e78529cc2e1cf634"
dependencies = [ dependencies = [
"base64 0.21.7",
"elements", "elements",
"elements-miniscript", "elements-miniscript",
"getrandom", "getrandom",
"qr_code",
"rand", "rand",
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "lwk_jade" name = "lwk_jade"
version = "0.3.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de8be1f26a3eceda9cc0234a346ca121337ae4206c487078e8c11df60529b0d" checksum = "fbe9d86bbe7584ff7081760b4476a47e2c1621a05dd9ad880c3ed5d46da1059b"
dependencies = [ dependencies = [
"elements", "elements",
"elements-miniscript", "elements-miniscript",
@@ -1381,13 +1418,12 @@ dependencies = [
[[package]] [[package]]
name = "lwk_signer" name = "lwk_signer"
version = "0.3.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626ed55ce98196605d54d75f10fc1d39f30813e42e6721c57046e7b0ad1f28cf" checksum = "06cab7a342823207e9a3f05ed2564547bbddf13fd61de75d2d5e1ef5bc45b241"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bip39", "bip39",
"elements",
"elements-miniscript", "elements-miniscript",
"lwk_common", "lwk_common",
"lwk_jade", "lwk_jade",
@@ -1396,21 +1432,23 @@ dependencies = [
[[package]] [[package]]
name = "lwk_wollet" name = "lwk_wollet"
version = "0.3.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a53624d988a635fee47e698258f9bab8dddc1ae73b5500ee0619a52d931c34" checksum = "4690a5a99097790e2411ae4a91ccf655a3d3e58610415e12a781dca9389930cb"
dependencies = [ dependencies = [
"aes-gcm-siv", "aes-gcm-siv",
"base64 0.21.7",
"bip39", "bip39",
"electrum-client", "electrum-client",
"elements", "elements",
"elements-miniscript", "elements-miniscript",
"fxhash",
"idna 0.4.0", "idna 0.4.0",
"lwk_common", "lwk_common",
"minreq",
"once_cell", "once_cell",
"rand", "rand",
"regex-lite", "regex-lite",
"reqwest",
"serde", "serde",
"serde_cbor", "serde_cbor",
"serde_json", "serde_json",
@@ -1450,21 +1488,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "minreq"
version = "2.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fdef521c74c2884a4f3570bcdb6d2a77b3c533feb6b27ac2ae72673cc221c64"
dependencies = [
"log",
"once_cell",
"rustls 0.21.12",
"rustls-webpki 0.101.7",
"serde",
"serde_json",
"webpki-roots 0.25.4",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.11"
@@ -1650,6 +1673,26 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.14"
@@ -1695,6 +1738,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "qr_code"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d2564aae5faaf3acb512b35b8bcb9a298d9d8c72d181c598691d800ee78a00"
dependencies = [
"bmp-monochrome",
]
[[package]] [[package]]
name = "qrcode-rs" name = "qrcode-rs"
version = "0.1.3" version = "0.1.3"
@@ -1802,41 +1854,46 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.27" version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.22.1",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http 0.2.12", "http",
"http-body", "http-body",
"http-body-util",
"hyper", "hyper",
"hyper-tls", "hyper-rustls",
"hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.22.4",
"rustls-pemfile", "rustls-pemfile",
"rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots 0.26.1",
"winreg", "winreg",
] ]
@@ -1926,11 +1983,12 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.4" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.22.1",
"rustls-pki-types",
] ]
[[package]] [[package]]
@@ -2345,12 +2403,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-rustls"
version = "0.3.1" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [ dependencies = [
"native-tls", "rustls 0.22.4",
"rustls-pki-types",
"tokio", "tokio",
] ]
@@ -2367,6 +2426,28 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@@ -2379,6 +2460,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@@ -2419,7 +2501,7 @@ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"data-encoding", "data-encoding",
"http 1.1.0", "http",
"httparse", "httparse",
"log", "log",
"native-tls", "native-tls",
@@ -2842,9 +2924,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.48.0", "windows-sys 0.48.0",

View File

@@ -145,8 +145,7 @@ pub(crate) fn handle_command(
command_result!(sdk.get_info(GetInfoRequest { with_scan: true })?) command_result!(sdk.get_info(GetInfoRequest { with_scan: true })?)
} }
Command::ListPayments => { Command::ListPayments => {
let mut payments = sdk.list_payments(true, true)?; let payments = sdk.list_payments()?;
payments.reverse();
command_result!(payments) command_result!(payments)
} }
Command::EmptyCache => { Command::EmptyCache => {

248
lib/Cargo.lock generated
View File

@@ -269,6 +269,12 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@@ -475,10 +481,16 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bmp-monochrome"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567"
[[package]] [[package]]
name = "boltz-client" name = "boltz-client"
version = "0.1.3" version = "0.1.3"
source = "git+https://github.com/hydra-yse/boltz-rust?rev=be8395900495e415699a54e15f4bd0bc6d774237#be8395900495e415699a54e15f4bd0bc6d774237" source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-05-21#a6254147a0d00756880f5de38ac6000e49f61560"
dependencies = [ dependencies = [
"bip39", "bip39",
"bitcoin 0.31.2", "bitcoin 0.31.2",
@@ -502,7 +514,6 @@ dependencies = [
"anyhow", "anyhow",
"bip39", "bip39",
"boltz-client", "boltz-client",
"elements",
"flutter_rust_bridge", "flutter_rust_bridge",
"glob", "glob",
"log", "log",
@@ -856,9 +867,9 @@ dependencies = [
[[package]] [[package]]
name = "elements-miniscript" name = "elements-miniscript"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73842aeed05c6d62a985672f651914080e6f1cedced5ea194405f5b5f838f6dd" checksum = "cd2c9c3a0acd5a30061dc5f1859b816ef01d320da66d074d9e23aad40e7a7eba"
dependencies = [ dependencies = [
"bitcoin 0.31.2", "bitcoin 0.31.2",
"elements", "elements",
@@ -1115,6 +1126,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@@ -1174,16 +1194,16 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.26" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
dependencies = [ dependencies = [
"atomic-waker",
"bytes", "bytes",
"fnv", "fnv",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "http",
"http 0.2.12",
"indexmap 2.2.6", "indexmap 2.2.6",
"slab", "slab",
"tokio", "tokio",
@@ -1267,17 +1287,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "http"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.1.0" version = "1.1.0"
@@ -1291,12 +1300,24 @@ dependencies = [
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.6" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [ dependencies = [
"bytes", "bytes",
"http 0.2.12", "http",
]
[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite", "pin-project-lite",
] ]
@@ -1306,12 +1327,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "1.3.0" version = "1.3.0"
@@ -1323,39 +1338,59 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.28" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http 0.2.12", "http",
"http-body", "http-body",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2", "smallvec",
"tokio", "tokio",
"tower-service",
"tracing",
"want", "want",
] ]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-rustls"
version = "0.5.0" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http",
"hyper",
"hyper-util",
"rustls 0.22.4",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper", "hyper",
"native-tls", "pin-project-lite",
"socket2",
"tokio", "tokio",
"tokio-native-tls", "tower",
"tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -1546,22 +1581,24 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "lwk_common" name = "lwk_common"
version = "0.3.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93bd7176520b15e692ee7d778f8f9fffe39a26b5df994d8f0e4295389576343" checksum = "451ed06b50b1bb76d6a910c36039a687f891868bd199c110e78529cc2e1cf634"
dependencies = [ dependencies = [
"base64 0.21.7",
"elements", "elements",
"elements-miniscript", "elements-miniscript",
"getrandom", "getrandom",
"qr_code",
"rand 0.8.5", "rand 0.8.5",
"thiserror", "thiserror",
] ]
[[package]] [[package]]
name = "lwk_jade" name = "lwk_jade"
version = "0.3.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de8be1f26a3eceda9cc0234a346ca121337ae4206c487078e8c11df60529b0d" checksum = "fbe9d86bbe7584ff7081760b4476a47e2c1621a05dd9ad880c3ed5d46da1059b"
dependencies = [ dependencies = [
"elements", "elements",
"elements-miniscript", "elements-miniscript",
@@ -1580,13 +1617,12 @@ dependencies = [
[[package]] [[package]]
name = "lwk_signer" name = "lwk_signer"
version = "0.3.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626ed55ce98196605d54d75f10fc1d39f30813e42e6721c57046e7b0ad1f28cf" checksum = "06cab7a342823207e9a3f05ed2564547bbddf13fd61de75d2d5e1ef5bc45b241"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bip39", "bip39",
"elements",
"elements-miniscript", "elements-miniscript",
"lwk_common", "lwk_common",
"lwk_jade", "lwk_jade",
@@ -1595,21 +1631,23 @@ dependencies = [
[[package]] [[package]]
name = "lwk_wollet" name = "lwk_wollet"
version = "0.3.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1a53624d988a635fee47e698258f9bab8dddc1ae73b5500ee0619a52d931c34" checksum = "4690a5a99097790e2411ae4a91ccf655a3d3e58610415e12a781dca9389930cb"
dependencies = [ dependencies = [
"aes-gcm-siv", "aes-gcm-siv",
"base64 0.21.7",
"bip39", "bip39",
"electrum-client", "electrum-client",
"elements", "elements",
"elements-miniscript", "elements-miniscript",
"fxhash",
"idna 0.4.0", "idna 0.4.0",
"lwk_common", "lwk_common",
"minreq",
"once_cell", "once_cell",
"rand 0.8.5", "rand 0.8.5",
"regex-lite", "regex-lite",
"reqwest",
"serde", "serde",
"serde_cbor", "serde_cbor",
"serde_json", "serde_json",
@@ -1665,21 +1703,6 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "minreq"
version = "2.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fdef521c74c2884a4f3570bcdb6d2a77b3c533feb6b27ac2ae72673cc221c64"
dependencies = [
"log",
"once_cell",
"rustls 0.21.12",
"rustls-webpki 0.101.7",
"serde",
"serde_json",
"webpki-roots 0.25.4",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.11"
@@ -1896,6 +1919,26 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.61",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.14"
@@ -1971,6 +2014,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "qr_code"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d2564aae5faaf3acb512b35b8bcb9a298d9d8c72d181c598691d800ee78a00"
dependencies = [
"bmp-monochrome",
]
[[package]] [[package]]
name = "quick-error" name = "quick-error"
version = "1.2.3" version = "1.2.3"
@@ -2117,41 +2169,46 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.27" version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.22.1",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",
"http 0.2.12", "http",
"http-body", "http-body",
"http-body-util",
"hyper", "hyper",
"hyper-tls", "hyper-rustls",
"hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"native-tls",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rustls 0.22.4",
"rustls-pemfile", "rustls-pemfile",
"rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots 0.26.1",
"winreg", "winreg",
] ]
@@ -2241,11 +2298,12 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pemfile" name = "rustls-pemfile"
version = "1.0.4" version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [ dependencies = [
"base64 0.21.7", "base64 0.22.1",
"rustls-pki-types",
] ]
[[package]] [[package]]
@@ -2754,12 +2812,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-rustls"
version = "0.3.1" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [ dependencies = [
"native-tls", "rustls 0.22.4",
"rustls-pki-types",
"tokio", "tokio",
] ]
@@ -2785,6 +2844,28 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@@ -2797,6 +2878,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [ dependencies = [
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@@ -2837,7 +2919,7 @@ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"data-encoding", "data-encoding",
"http 1.1.0", "http",
"httparse", "httparse",
"log", "log",
"native-tls", "native-tls",
@@ -3636,9 +3718,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.48.0", "windows-sys 0.48.0",

View File

@@ -58,12 +58,13 @@ typedef struct wire_cst_prepare_send_response {
} wire_cst_prepare_send_response; } wire_cst_prepare_send_response;
typedef struct wire_cst_payment { typedef struct wire_cst_payment {
struct wire_cst_list_prim_u_8_strict *id; struct wire_cst_list_prim_u_8_strict *tx_id;
uint32_t *timestamp; struct wire_cst_list_prim_u_8_strict *swap_id;
uint32_t timestamp;
uint64_t amount_sat; uint64_t amount_sat;
uint64_t *fees_sat; uint64_t *fees_sat;
int32_t payment_type; int32_t payment_type;
struct wire_cst_list_prim_u_8_strict *invoice; int32_t status;
} wire_cst_payment; } wire_cst_payment;
typedef struct wire_cst_list_payment { typedef struct wire_cst_list_payment {
@@ -73,6 +74,8 @@ typedef struct wire_cst_list_payment {
typedef struct wire_cst_get_info_response { typedef struct wire_cst_get_info_response {
uint64_t balance_sat; uint64_t balance_sat;
uint64_t pending_send_sat;
uint64_t pending_receive_sat;
struct wire_cst_list_prim_u_8_strict *pubkey; struct wire_cst_list_prim_u_8_strict *pubkey;
} wire_cst_get_info_response; } wire_cst_get_info_response;
@@ -86,7 +89,7 @@ typedef struct wire_cst_PaymentError_LwkError {
typedef struct wire_cst_PaymentError_Refunded { typedef struct wire_cst_PaymentError_Refunded {
struct wire_cst_list_prim_u_8_strict *err; struct wire_cst_list_prim_u_8_strict *err;
struct wire_cst_list_prim_u_8_strict *txid; struct wire_cst_list_prim_u_8_strict *refund_tx_id;
} wire_cst_PaymentError_Refunded; } wire_cst_PaymentError_Refunded;
typedef struct wire_cst_PaymentError_SendError { typedef struct wire_cst_PaymentError_SendError {
@@ -127,7 +130,7 @@ void frbgen_breez_liquid_wire_empty_wallet_cache(int64_t port_);
void frbgen_breez_liquid_wire_get_info(int64_t port_, struct wire_cst_get_info_request *req); void frbgen_breez_liquid_wire_get_info(int64_t port_, struct wire_cst_get_info_request *req);
void frbgen_breez_liquid_wire_list_payments(int64_t port_, bool with_scan, bool include_pending); void frbgen_breez_liquid_wire_list_payments(int64_t port_);
void frbgen_breez_liquid_wire_prepare_receive_payment(int64_t port_, void frbgen_breez_liquid_wire_prepare_receive_payment(int64_t port_,
struct wire_cst_prepare_receive_request *req); struct wire_cst_prepare_receive_request *req);
@@ -157,8 +160,6 @@ struct wire_cst_prepare_send_response *frbgen_breez_liquid_cst_new_box_autoadd_p
struct wire_cst_restore_request *frbgen_breez_liquid_cst_new_box_autoadd_restore_request(void); struct wire_cst_restore_request *frbgen_breez_liquid_cst_new_box_autoadd_restore_request(void);
uint32_t *frbgen_breez_liquid_cst_new_box_autoadd_u_32(uint32_t value);
uint64_t *frbgen_breez_liquid_cst_new_box_autoadd_u_64(uint64_t value); uint64_t *frbgen_breez_liquid_cst_new_box_autoadd_u_64(uint64_t value);
struct wire_cst_list_payment *frbgen_breez_liquid_cst_new_list_payment(int32_t len); struct wire_cst_list_payment *frbgen_breez_liquid_cst_new_list_payment(int32_t len);
@@ -173,7 +174,6 @@ static int64_t dummy_method_to_enforce_bundling(void) {
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_send_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_send_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_send_response); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_prepare_send_response);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_restore_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_restore_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_u_32);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_u_64); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_u_64);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_payment); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_payment);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_prim_u_8_strict); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_list_prim_u_8_strict);

View File

@@ -37,6 +37,8 @@ dictionary GetInfoRequest {
dictionary GetInfoResponse { dictionary GetInfoResponse {
u64 balance_sat; u64 balance_sat;
u64 pending_send_sat;
u64 pending_receive_sat;
string pubkey; string pubkey;
}; };
@@ -92,6 +94,9 @@ interface BindingLiquidSdk {
[Throws=PaymentError] [Throws=PaymentError]
ReceivePaymentResponse receive_payment(PrepareReceiveResponse req); ReceivePaymentResponse receive_payment(PrepareReceiveResponse req);
[Throws=LiquidSdkError]
void sync();
[Throws=LiquidSdkError] [Throws=LiquidSdkError]
void backup(); void backup();

View File

@@ -45,6 +45,10 @@ impl BindingLiquidSdk {
self.sdk.receive_payment(&req) self.sdk.receive_payment(&req)
} }
pub fn sync(&self) -> Result<(), LiquidSdkError> {
self.sdk.sync().map_err(Into::into)
}
pub fn backup(&self) -> Result<(), LiquidSdkError> { pub fn backup(&self) -> Result<(), LiquidSdkError> {
self.sdk.backup().map_err(Into::into) self.sdk.backup().map_err(Into::into)
} }

View File

@@ -15,12 +15,12 @@ frb = ["dep:flutter_rust_bridge"]
anyhow = { workspace = true } anyhow = { workspace = true }
bip39 = { version = "2.0.0", features = ["serde"] } bip39 = { version = "2.0.0", features = ["serde"] }
#boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", rev = "a05731cc33030ada9ae14afcafe0cded22842ba6" } #boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", rev = "a05731cc33030ada9ae14afcafe0cded22842ba6" }
boltz-client = { git = "https://github.com/hydra-yse/boltz-rust", rev = "be8395900495e415699a54e15f4bd0bc6d774237" } boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-breez-latest-05-21" }
flutter_rust_bridge = { version = "=2.0.0-dev.33", features = ["chrono"], optional = true } flutter_rust_bridge = { version = "=2.0.0-dev.33", features = ["chrono"], optional = true }
log = "0.4.20" log = "0.4.20"
lwk_common = "0.3.0" lwk_common = "0.5.0"
lwk_signer = "0.3.0" lwk_signer = "0.5.0"
lwk_wollet = "0.3.0" lwk_wollet = "0.5.0"
rusqlite = { version = "0.31", features = ["backup", "bundled"] } rusqlite = { version = "0.31", features = ["backup", "bundled"] }
rusqlite_migration = "1.0" rusqlite_migration = "1.0"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
@@ -28,8 +28,6 @@ serde_json = "1.0.116"
thiserror = { workspace = true } thiserror = { workspace = true }
tungstenite = { version = "0.21.0", features = ["native-tls-vendored"] } tungstenite = { version = "0.21.0", features = ["native-tls-vendored"] }
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
# TODO Remove once fully migrated to v2 API
elements = "0.24.1"
[dev-dependencies] [dev-dependencies]
tempdir = "0.3.7" tempdir = "0.3.7"

View File

@@ -57,12 +57,12 @@ pub fn receive_payment(
.receive_payment(&req) .receive_payment(&req)
} }
pub fn list_payments(with_scan: bool, include_pending: bool) -> Result<Vec<Payment>> { pub fn list_payments() -> Result<Vec<Payment>> {
LIQUID_SDK_INSTANCE LIQUID_SDK_INSTANCE
.get() .get()
.ok_or(anyhow!("Not initialized")) .ok_or(anyhow!("Not initialized"))
.map_err(|e| LiquidSdkError::Generic { err: e.to_string() })? .map_err(|e| LiquidSdkError::Generic { err: e.to_string() })?
.list_payments(with_scan, include_pending) .list_payments()
} }
pub fn empty_wallet_cache() -> Result<()> { pub fn empty_wallet_cache() -> Result<()> {

View File

@@ -2,8 +2,9 @@ use std::collections::HashSet;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::net::TcpStream; use std::net::TcpStream;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex, OnceLock};
use std::thread; use std::thread;
use std::time::{Duration, Instant};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use boltz_client::swaps::{ use boltz_client::swaps::{
@@ -11,99 +12,97 @@ use boltz_client::swaps::{
boltzv2::{Subscription, SwapUpdate}, boltzv2::{Subscription, SwapUpdate},
}; };
use boltz_client::SwapType; use boltz_client::SwapType;
use log::{error, info, warn}; use log::{debug, error, info, warn};
use tungstenite::stream::MaybeTlsStream; use tungstenite::stream::MaybeTlsStream;
use tungstenite::{Message, WebSocket}; use tungstenite::{Message, WebSocket};
use crate::model::*; use crate::model::*;
use crate::sdk::LiquidSdk; use crate::sdk::LiquidSdk;
pub(super) struct BoltzStatusStream { static SWAP_IN_IDS: OnceLock<Arc<Mutex<HashSet<String>>>> = OnceLock::new();
swap_in_ids: Arc<Mutex<HashSet<String>>>, static SWAP_OUT_IDS: OnceLock<Arc<Mutex<HashSet<String>>>> = OnceLock::new();
swap_out_ids: Arc<Mutex<HashSet<String>>>,
fn swap_in_ids() -> &'static Arc<Mutex<HashSet<String>>> {
let swap_in_ids = Default::default();
SWAP_IN_IDS.get_or_init(|| swap_in_ids)
} }
fn swap_out_ids() -> &'static Arc<Mutex<HashSet<String>>> {
let swap_out_ids = Default::default();
SWAP_OUT_IDS.get_or_init(|| swap_out_ids)
}
/// Set underlying TCP stream to nonblocking mode.
///
/// This allows us to `read()` without blocking.
pub(crate) fn set_stream_nonblocking(stream: &mut MaybeTlsStream<TcpStream>) -> Result<()> {
match stream {
tungstenite::stream::MaybeTlsStream::Plain(s) => s.set_nonblocking(true)?,
tungstenite::stream::MaybeTlsStream::NativeTls(s) => s.get_mut().set_nonblocking(true)?,
_ => Err(anyhow!("Unsupported stream type"))?,
};
Ok(())
}
pub(super) struct BoltzStatusStream {}
impl BoltzStatusStream { impl BoltzStatusStream {
pub(super) fn new() -> Self { pub(super) fn mark_swap_as_tracked(id: &str, swap_type: SwapType) {
BoltzStatusStream {
swap_in_ids: Default::default(),
swap_out_ids: Default::default(),
}
}
pub(super) fn insert_tracked_swap(&self, id: &str, swap_type: SwapType) {
match swap_type { match swap_type {
SwapType::Submarine => self.swap_in_ids.lock().unwrap().insert(id.to_string()), SwapType::Submarine => swap_in_ids().lock().unwrap().insert(id.to_string()),
SwapType::ReverseSubmarine => self.swap_out_ids.lock().unwrap().insert(id.to_string()), SwapType::ReverseSubmarine => swap_out_ids().lock().unwrap().insert(id.to_string()),
}; };
} }
pub(super) fn resolve_tracked_swap(&self, id: &str, swap_type: SwapType) { pub(super) fn unmark_swap_as_tracked(id: &str, swap_type: SwapType) {
match swap_type { match swap_type {
SwapType::Submarine => self.swap_in_ids.lock().unwrap().remove(id), SwapType::Submarine => swap_in_ids().lock().unwrap().remove(id),
SwapType::ReverseSubmarine => self.swap_out_ids.lock().unwrap().remove(id), SwapType::ReverseSubmarine => swap_out_ids().lock().unwrap().remove(id),
}; };
} }
pub(super) fn track_pending_swaps(&self, sdk: Arc<LiquidSdk>) -> Result<()> { fn connect(sdk: Arc<LiquidSdk>) -> Result<WebSocket<MaybeTlsStream<TcpStream>>> {
// Track subscribed swap IDs
let mut socket = sdk let mut socket = sdk
.boltz_client_v2() .boltz_client_v2()
.connect_ws() .connect_ws()
.map_err(|e| anyhow!("Failed to connect to websocket: {e:?}"))?; .map_err(|e| anyhow!("Failed to connect to websocket: {e:?}"))?;
set_stream_nonblocking(socket.get_mut())?;
Ok(socket)
}
// Set underlying TCP stream to nonblocking mode pub(super) fn track_pending_swaps(sdk: Arc<LiquidSdk>) -> Result<()> {
match socket.get_mut() { let mut socket = Self::connect(sdk.clone())?;
tungstenite::stream::MaybeTlsStream::Plain(s) => s.set_nonblocking(true)?,
tungstenite::stream::MaybeTlsStream::NativeTls(s) => {
s.get_mut().set_nonblocking(true)?
}
_ => Err(anyhow!("Unsupported stream type"))?,
};
let swap_in_ids = self.swap_in_ids.clone(); let reconnect_delay = Duration::from_secs(15);
let swap_out_ids = self.swap_out_ids.clone(); let keep_alive_ping_interval = Duration::from_secs(15);
let mut keep_alive_last_ping_ts = Instant::now();
// Outer loop: reconnects in case the connection is lost
thread::spawn(move || loop { thread::spawn(move || loop {
let maybe_subscribe_fn =
|ongoing_swap: &OngoingSwap, socket: &mut WebSocket<MaybeTlsStream<TcpStream>>| {
let id = ongoing_swap.id();
let is_ongoing_swap_already_tracked = match ongoing_swap {
OngoingSwap::Send(_) => swap_in_ids.lock().unwrap().contains(&id),
OngoingSwap::Receive(_) => swap_out_ids.lock().unwrap().contains(&id),
};
if !is_ongoing_swap_already_tracked {
info!("Subscribing to status for ongoing swap ID {id}");
let subscription = Subscription::new(&id);
let subscribe_json = serde_json::to_string(&subscription)
.map_err(|e| anyhow!("Invalid subscription msg: {e:?}"))
.unwrap();
socket
.send(tungstenite::Message::Text(subscribe_json))
.map_err(|e| anyhow!("Failed to subscribe to {id}: {e:?}"))
.unwrap();
match ongoing_swap {
OngoingSwap::Send(_) => swap_in_ids.lock().unwrap().insert(id),
OngoingSwap::Receive(_) => swap_out_ids.lock().unwrap().insert(id),
};
}
};
// Initially subscribe to all ongoing swaps // Initially subscribe to all ongoing swaps
match sdk.list_ongoing_swaps() { match sdk.list_ongoing_swaps() {
Ok(initial_ongoing_swaps) => { Ok(initial_ongoing_swaps) => {
info!("Got {} initial ongoing swaps", initial_ongoing_swaps.len()); info!("Got {} initial ongoing swaps", initial_ongoing_swaps.len());
for ongoing_swap in &initial_ongoing_swaps { for ongoing_swap in &initial_ongoing_swaps {
maybe_subscribe_fn(ongoing_swap, &mut socket); Self::maybe_subscribe_fn(ongoing_swap, &mut socket);
} }
} }
Err(e) => error!("Failed to list initial ongoing swaps: {e:?}"), Err(e) => error!("Failed to list initial ongoing swaps: {e:?}"),
} }
// Inner loop: iterates over incoming messages and handles them
loop { loop {
// Decide if we send a keep-alive ping or not
if Instant::now()
.duration_since(keep_alive_last_ping_ts)
.gt(&keep_alive_ping_interval)
{
match socket.send(Message::Ping(vec![])) {
Ok(_) => debug!("Sent keep-alive ping"),
Err(e) => warn!("Failed to send keep-alive ping: {e:?}"),
}
keep_alive_last_ping_ts = Instant::now();
}
match &socket.read() { match &socket.read() {
Ok(Message::Close(_)) => { Ok(Message::Close(_)) => {
warn!("Received close msg, exiting socket loop"); warn!("Received close msg, exiting socket loop");
@@ -119,7 +118,7 @@ impl BoltzStatusStream {
match sdk.list_ongoing_swaps() { match sdk.list_ongoing_swaps() {
Ok(ongoing_swaps) => { Ok(ongoing_swaps) => {
for ongoing_swap in &ongoing_swaps { for ongoing_swap in &ongoing_swaps {
maybe_subscribe_fn(ongoing_swap, &mut socket); Self::maybe_subscribe_fn(ongoing_swap, &mut socket);
} }
} }
Err(e) => error!("Failed to list new ongoing swaps: {e:?}"), Err(e) => error!("Failed to list new ongoing swaps: {e:?}"),
@@ -127,62 +126,59 @@ impl BoltzStatusStream {
// We parse and handle any Text websocket messages, which are likely status updates // We parse and handle any Text websocket messages, which are likely status updates
if msg.is_text() { if msg.is_text() {
let response: SwapUpdate = serde_json::from_str(&msg.to_string()) info!("Received text msg (status update) : {msg:?}");
.map_err(|e| anyhow!("WS response is invalid SwapUpdate: {e:?}"))
.unwrap();
info!("Received update : {response:?}");
match response { match serde_json::from_str::<SwapUpdate>(&msg.to_string()) {
// Subscription confirmation // Subscription confirmation
boltz_client::swaps::boltzv2::SwapUpdate::Subscription { Ok(SwapUpdate::Subscription { .. }) => {}
..
} => {}
// Status update // Status update(s)
boltz_client::swaps::boltzv2::SwapUpdate::Update { Ok(SwapUpdate::Update {
event: _, event: _,
channel: _, channel: _,
args, args,
} => { }) => {
let update = args.first().unwrap().clone(); // TODO for boltz_client::swaps::boltzv2::Update { id, status } in args
let update_swap_id = update.id.clone();
let update_state_str = update.status.clone();
if swap_in_ids.lock().unwrap().contains(&update_swap_id) {
// Known OngoingSwapIn / Send swap
match SubSwapStates::from_str(&update_state_str) {
Ok(new_state) => {
let res = sdk.try_handle_submarine_swap_status(
new_state,
&update_swap_id,
);
info!("ongoingswapin / send try_handle_submarine_swap_status res: {res:?}");
}
Err(_) => error!("Invalid state for submarine swap {update_swap_id}: {update_state_str}")
}
} else if swap_out_ids.lock().unwrap().contains(&update_swap_id)
{ {
// Known OngoingSwapOut / receive swap if Self::is_tracked_swap_in(&id) {
// Known OngoingSwapIn / Send swap
match RevSwapStates::from_str(&update_state_str) { match SubSwapStates::from_str(&status) {
Ok(new_state) => { Ok(new_state) => {
let res = sdk.try_handle_reverse_swap_status( let res = sdk.try_handle_submarine_swap_status(
new_state, new_state,
&update_swap_id, &id,
); );
info!("OngoingSwapOut / receive try_handle_reverse_swap_status res: {res:?}"); info!("OngoingSwapIn / send try_handle_submarine_swap_status res: {res:?}");
}
Err(_) => error!("Received invalid SubSwapState for swap {id}: {status}")
} }
Err(_) => error!("Invalid state for reverse swap {update_swap_id}: {update_state_str}") } else if Self::is_tracked_swap_out(&id) {
// Known OngoingSwapOut / receive swap
match RevSwapStates::from_str(&status) {
Ok(new_state) => {
let res = sdk.try_handle_reverse_swap_status(
new_state, &id,
);
info!("OngoingSwapOut / receive try_handle_reverse_swap_status res: {res:?}");
}
Err(_) => error!("Received invalid RevSwapState for swap {id}: {status}"),
}
} else {
warn!("Received a status update for swap {id}, which is not tracked as ongoing")
} }
} else {
// We got an update for a swap we did not track as ongoing
todo!()
} }
} }
// Error related to subscription, like "Unknown swap ID" // Error related to subscription, like "Unknown swap ID"
boltz_client::swaps::boltzv2::SwapUpdate::Error { .. } => todo!(), Ok(SwapUpdate::Error {
event: _,
channel: _,
args,
}) => error!("Received a status update error: {args:?}"),
Err(e) => warn!("WS response is invalid SwapUpdate: {e:?}"),
} }
} }
} }
@@ -198,6 +194,22 @@ impl BoltzStatusStream {
} }
} }
} }
Err(tungstenite::Error::AlreadyClosed) => {
thread::sleep(reconnect_delay);
info!("Re-connecting...");
match Self::connect(sdk.clone()) {
Ok(new_socket) => {
socket = new_socket;
info!("Re-connected to WS stream");
// Clear monitored swaps, so on re-connect we re-subscribe to them
swap_in_ids().lock().unwrap().clear();
swap_out_ids().lock().unwrap().clear();
}
Err(e) => warn!("Failed to re-connected to WS stream: {e:}"),
};
break;
}
Err(e) => { Err(e) => {
error!("Received stream error : {e:?}"); error!("Received stream error : {e:?}");
break; break;
@@ -208,4 +220,33 @@ impl BoltzStatusStream {
Ok(()) Ok(())
} }
fn is_tracked_swap_in(id: &str) -> bool {
swap_in_ids().lock().unwrap().contains(id)
}
fn is_tracked_swap_out(id: &str) -> bool {
swap_out_ids().lock().unwrap().contains(id)
}
fn maybe_subscribe_fn(swap: &Swap, socket: &mut WebSocket<MaybeTlsStream<TcpStream>>) {
let id = swap.id();
let is_ongoing_swap_already_tracked = match swap {
Swap::Send(_) => Self::is_tracked_swap_in(&id),
Swap::Receive(_) => Self::is_tracked_swap_out(&id),
};
if !is_ongoing_swap_already_tracked {
info!("Subscribing to status updates for ongoing swap ID {id}");
let subscription = Subscription::new(&id);
match serde_json::to_string(&subscription) {
Ok(subscribe_json) => match socket.send(Message::Text(subscribe_json)) {
Ok(_) => Self::mark_swap_as_tracked(&id, swap.swap_type()),
Err(e) => error!("Failed to subscribe to {id}: {e:?}"),
},
Err(e) => error!("Invalid subscription msg: {e:?}"),
}
}
}
} }

View File

@@ -55,7 +55,7 @@ pub enum PaymentError {
PersistError, PersistError,
#[error("The payment has been refunded. Reason for failure: {err}")] #[error("The payment has been refunded. Reason for failure: {err}")]
Refunded { err: String, txid: String }, Refunded { err: String, refund_tx_id: String },
#[error("Could not sign/send the transaction: {err}")] #[error("Could not sign/send the transaction: {err}")]
SendError { err: String }, SendError { err: String },
@@ -67,20 +67,9 @@ pub enum PaymentError {
impl From<boltz_client::error::Error> for PaymentError { impl From<boltz_client::error::Error> for PaymentError {
fn from(err: boltz_client::error::Error) -> Self { fn from(err: boltz_client::error::Error) -> Self {
match err { match err {
boltz_client::error::Error::Protocol(msg) => { boltz_client::error::Error::HTTP(e) => PaymentError::Generic {
if msg == "Could not find utxos for script" { err: format!("Could not contact servers: {e:?}"),
return PaymentError::AlreadyClaimed; },
}
PaymentError::Generic { err: msg }
}
boltz_client::error::Error::HTTP(ureq) => {
dbg!(ureq.into_response().unwrap().into_string().unwrap());
PaymentError::Generic {
err: "Could not contact servers".to_string(),
}
}
_ => PaymentError::Generic { _ => PaymentError::Generic {
err: format!("{err:?}"), err: format!("{err:?}"),
}, },

View File

@@ -78,12 +78,6 @@ impl CstDecode<crate::model::RestoreRequest> for *mut wire_cst_restore_request {
CstDecode::<crate::model::RestoreRequest>::cst_decode(*wrap).into() CstDecode::<crate::model::RestoreRequest>::cst_decode(*wrap).into()
} }
} }
impl CstDecode<u32> for *mut u32 {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> u32 {
unsafe { *flutter_rust_bridge::for_generated::box_from_leak_ptr(self) }
}
}
impl CstDecode<u64> for *mut u64 { impl CstDecode<u64> for *mut u64 {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> u64 { fn cst_decode(self) -> u64 {
@@ -113,6 +107,8 @@ impl CstDecode<crate::model::GetInfoResponse> for wire_cst_get_info_response {
fn cst_decode(self) -> crate::model::GetInfoResponse { fn cst_decode(self) -> crate::model::GetInfoResponse {
crate::model::GetInfoResponse { crate::model::GetInfoResponse {
balance_sat: self.balance_sat.cst_decode(), balance_sat: self.balance_sat.cst_decode(),
pending_send_sat: self.pending_send_sat.cst_decode(),
pending_receive_sat: self.pending_receive_sat.cst_decode(),
pubkey: self.pubkey.cst_decode(), pubkey: self.pubkey.cst_decode(),
} }
} }
@@ -140,12 +136,13 @@ impl CstDecode<crate::model::Payment> for wire_cst_payment {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::Payment { fn cst_decode(self) -> crate::model::Payment {
crate::model::Payment { crate::model::Payment {
id: self.id.cst_decode(), tx_id: self.tx_id.cst_decode(),
swap_id: self.swap_id.cst_decode(),
timestamp: self.timestamp.cst_decode(), timestamp: self.timestamp.cst_decode(),
amount_sat: self.amount_sat.cst_decode(), amount_sat: self.amount_sat.cst_decode(),
fees_sat: self.fees_sat.cst_decode(), fees_sat: self.fees_sat.cst_decode(),
payment_type: self.payment_type.cst_decode(), payment_type: self.payment_type.cst_decode(),
invoice: self.invoice.cst_decode(), status: self.status.cst_decode(),
} }
} }
} }
@@ -177,7 +174,7 @@ impl CstDecode<crate::error::PaymentError> for wire_cst_payment_error {
let ans = unsafe { self.kind.Refunded }; let ans = unsafe { self.kind.Refunded };
crate::error::PaymentError::Refunded { crate::error::PaymentError::Refunded {
err: ans.err.cst_decode(), err: ans.err.cst_decode(),
txid: ans.txid.cst_decode(), refund_tx_id: ans.refund_tx_id.cst_decode(),
} }
} }
11 => { 11 => {
@@ -285,6 +282,8 @@ impl NewWithNullPtr for wire_cst_get_info_response {
fn new_with_null_ptr() -> Self { fn new_with_null_ptr() -> Self {
Self { Self {
balance_sat: Default::default(), balance_sat: Default::default(),
pending_send_sat: Default::default(),
pending_receive_sat: Default::default(),
pubkey: core::ptr::null_mut(), pubkey: core::ptr::null_mut(),
} }
} }
@@ -297,12 +296,13 @@ impl Default for wire_cst_get_info_response {
impl NewWithNullPtr for wire_cst_payment { impl NewWithNullPtr for wire_cst_payment {
fn new_with_null_ptr() -> Self { fn new_with_null_ptr() -> Self {
Self { Self {
id: core::ptr::null_mut(), tx_id: core::ptr::null_mut(),
timestamp: core::ptr::null_mut(), swap_id: core::ptr::null_mut(),
timestamp: Default::default(),
amount_sat: Default::default(), amount_sat: Default::default(),
fees_sat: core::ptr::null_mut(), fees_sat: core::ptr::null_mut(),
payment_type: Default::default(), payment_type: Default::default(),
invoice: core::ptr::null_mut(), status: Default::default(),
} }
} }
} }
@@ -436,12 +436,8 @@ pub extern "C" fn frbgen_breez_liquid_wire_get_info(
} }
#[no_mangle] #[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire_list_payments( pub extern "C" fn frbgen_breez_liquid_wire_list_payments(port_: i64) {
port_: i64, wire_list_payments_impl(port_)
with_scan: bool,
include_pending: bool,
) {
wire_list_payments_impl(port_, with_scan, include_pending)
} }
#[no_mangle] #[no_mangle]
@@ -537,11 +533,6 @@ pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_restore_request(
) )
} }
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_u_32(value: u32) -> *mut u32 {
flutter_rust_bridge::for_generated::new_leak_box_ptr(value)
}
#[no_mangle] #[no_mangle]
pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_u_64(value: u64) -> *mut u64 { pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_u_64(value: u64) -> *mut u64 {
flutter_rust_bridge::for_generated::new_leak_box_ptr(value) flutter_rust_bridge::for_generated::new_leak_box_ptr(value)
@@ -586,6 +577,8 @@ pub struct wire_cst_get_info_request {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_get_info_response { pub struct wire_cst_get_info_response {
balance_sat: u64, balance_sat: u64,
pending_send_sat: u64,
pending_receive_sat: u64,
pubkey: *mut wire_cst_list_prim_u_8_strict, pubkey: *mut wire_cst_list_prim_u_8_strict,
} }
#[repr(C)] #[repr(C)]
@@ -603,12 +596,13 @@ pub struct wire_cst_list_prim_u_8_strict {
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_payment { pub struct wire_cst_payment {
id: *mut wire_cst_list_prim_u_8_strict, tx_id: *mut wire_cst_list_prim_u_8_strict,
timestamp: *mut u32, swap_id: *mut wire_cst_list_prim_u_8_strict,
timestamp: u32,
amount_sat: u64, amount_sat: u64,
fees_sat: *mut u64, fees_sat: *mut u64,
payment_type: i32, payment_type: i32,
invoice: *mut wire_cst_list_prim_u_8_strict, status: i32,
} }
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -640,7 +634,7 @@ pub struct wire_cst_PaymentError_LwkError {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_PaymentError_Refunded { pub struct wire_cst_PaymentError_Refunded {
err: *mut wire_cst_list_prim_u_8_strict, err: *mut wire_cst_list_prim_u_8_strict,
txid: *mut wire_cst_list_prim_u_8_strict, refund_tx_id: *mut wire_cst_list_prim_u_8_strict,
} }
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]

View File

@@ -93,26 +93,14 @@ fn wire_get_info_impl(
}, },
) )
} }
fn wire_list_payments_impl( fn wire_list_payments_impl(port_: flutter_rust_bridge::for_generated::MessagePort) {
port_: flutter_rust_bridge::for_generated::MessagePort,
with_scan: impl CstDecode<bool>,
include_pending: impl CstDecode<bool>,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>( FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo { flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "list_payments", debug_name: "list_payments",
port: Some(port_), port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
}, },
move || { move || move |context| transform_result_dco((move || crate::bindings::list_payments())()),
let api_with_scan = with_scan.cst_decode();
let api_include_pending = include_pending.cst_decode();
move |context| {
transform_result_dco((move || {
crate::bindings::list_payments(api_with_scan, api_include_pending)
})())
}
},
) )
} }
fn wire_prepare_receive_payment_impl( fn wire_prepare_receive_payment_impl(
@@ -226,14 +214,24 @@ impl CstDecode<crate::model::Network> for i32 {
} }
} }
} }
impl CstDecode<crate::model::PaymentState> for i32 {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::PaymentState {
match self {
0 => crate::model::PaymentState::Created,
1 => crate::model::PaymentState::Pending,
2 => crate::model::PaymentState::Complete,
3 => crate::model::PaymentState::Failed,
_ => unreachable!("Invalid variant for NewSwapState: {}", self),
}
}
}
impl CstDecode<crate::model::PaymentType> for i32 { impl CstDecode<crate::model::PaymentType> for i32 {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::PaymentType { fn cst_decode(self) -> crate::model::PaymentType {
match self { match self {
0 => crate::model::PaymentType::Sent, 0 => crate::model::PaymentType::Receive,
1 => crate::model::PaymentType::Received, 1 => crate::model::PaymentType::Send,
2 => crate::model::PaymentType::PendingReceive,
3 => crate::model::PaymentType::PendingSend,
_ => unreachable!("Invalid variant for PaymentType: {}", self), _ => unreachable!("Invalid variant for PaymentType: {}", self),
} }
} }
@@ -306,9 +304,13 @@ impl SseDecode for crate::model::GetInfoResponse {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_balanceSat = <u64>::sse_decode(deserializer); let mut var_balanceSat = <u64>::sse_decode(deserializer);
let mut var_pendingSendSat = <u64>::sse_decode(deserializer);
let mut var_pendingReceiveSat = <u64>::sse_decode(deserializer);
let mut var_pubkey = <String>::sse_decode(deserializer); let mut var_pubkey = <String>::sse_decode(deserializer);
return crate::model::GetInfoResponse { return crate::model::GetInfoResponse {
balance_sat: var_balanceSat, balance_sat: var_balanceSat,
pending_send_sat: var_pendingSendSat,
pending_receive_sat: var_pendingReceiveSat,
pubkey: var_pubkey, pubkey: var_pubkey,
}; };
} }
@@ -357,6 +359,20 @@ impl SseDecode for crate::model::Network {
} }
} }
impl SseDecode for crate::model::PaymentState {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <i32>::sse_decode(deserializer);
return match inner {
0 => crate::model::PaymentState::Created,
1 => crate::model::PaymentState::Pending,
2 => crate::model::PaymentState::Complete,
3 => crate::model::PaymentState::Failed,
_ => unreachable!("Invalid variant for NewSwapState: {}", inner),
};
}
}
impl SseDecode for Option<String> { impl SseDecode for Option<String> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -368,17 +384,6 @@ impl SseDecode for Option<String> {
} }
} }
impl SseDecode for Option<u32> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<u32>::sse_decode(deserializer));
} else {
return None;
}
}
}
impl SseDecode for Option<u64> { impl SseDecode for Option<u64> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -393,19 +398,21 @@ impl SseDecode for Option<u64> {
impl SseDecode for crate::model::Payment { impl SseDecode for crate::model::Payment {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_id = <Option<String>>::sse_decode(deserializer); let mut var_txId = <String>::sse_decode(deserializer);
let mut var_timestamp = <Option<u32>>::sse_decode(deserializer); let mut var_swapId = <Option<String>>::sse_decode(deserializer);
let mut var_timestamp = <u32>::sse_decode(deserializer);
let mut var_amountSat = <u64>::sse_decode(deserializer); let mut var_amountSat = <u64>::sse_decode(deserializer);
let mut var_feesSat = <Option<u64>>::sse_decode(deserializer); let mut var_feesSat = <Option<u64>>::sse_decode(deserializer);
let mut var_paymentType = <crate::model::PaymentType>::sse_decode(deserializer); let mut var_paymentType = <crate::model::PaymentType>::sse_decode(deserializer);
let mut var_invoice = <Option<String>>::sse_decode(deserializer); let mut var_status = <crate::model::PaymentState>::sse_decode(deserializer);
return crate::model::Payment { return crate::model::Payment {
id: var_id, tx_id: var_txId,
swap_id: var_swapId,
timestamp: var_timestamp, timestamp: var_timestamp,
amount_sat: var_amountSat, amount_sat: var_amountSat,
fees_sat: var_feesSat, fees_sat: var_feesSat,
payment_type: var_paymentType, payment_type: var_paymentType,
invoice: var_invoice, status: var_status,
}; };
} }
} }
@@ -449,10 +456,10 @@ impl SseDecode for crate::error::PaymentError {
} }
10 => { 10 => {
let mut var_err = <String>::sse_decode(deserializer); let mut var_err = <String>::sse_decode(deserializer);
let mut var_txid = <String>::sse_decode(deserializer); let mut var_refundTxId = <String>::sse_decode(deserializer);
return crate::error::PaymentError::Refunded { return crate::error::PaymentError::Refunded {
err: var_err, err: var_err,
txid: var_txid, refund_tx_id: var_refundTxId,
}; };
} }
11 => { 11 => {
@@ -475,10 +482,8 @@ impl SseDecode for crate::model::PaymentType {
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <i32>::sse_decode(deserializer); let mut inner = <i32>::sse_decode(deserializer);
return match inner { return match inner {
0 => crate::model::PaymentType::Sent, 0 => crate::model::PaymentType::Receive,
1 => crate::model::PaymentType::Received, 1 => crate::model::PaymentType::Send,
2 => crate::model::PaymentType::PendingReceive,
3 => crate::model::PaymentType::PendingSend,
_ => unreachable!("Invalid variant for PaymentType: {}", inner), _ => unreachable!("Invalid variant for PaymentType: {}", inner),
}; };
} }
@@ -649,6 +654,8 @@ impl flutter_rust_bridge::IntoDart for crate::model::GetInfoResponse {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
self.balance_sat.into_into_dart().into_dart(), self.balance_sat.into_into_dart().into_dart(),
self.pending_send_sat.into_into_dart().into_dart(),
self.pending_receive_sat.into_into_dart().into_dart(),
self.pubkey.into_into_dart().into_dart(), self.pubkey.into_into_dart().into_dart(),
] ]
.into_dart() .into_dart()
@@ -678,15 +685,33 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::Network> for crate::model::
} }
} }
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::PaymentState {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
Self::Created => 0.into_dart(),
Self::Pending => 1.into_dart(),
Self::Complete => 2.into_dart(),
Self::Failed => 3.into_dart(),
}
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::model::PaymentState {}
impl flutter_rust_bridge::IntoIntoDart<crate::model::PaymentState> for crate::model::PaymentState {
fn into_into_dart(self) -> crate::model::PaymentState {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::Payment { impl flutter_rust_bridge::IntoDart for crate::model::Payment {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
self.id.into_into_dart().into_dart(), self.tx_id.into_into_dart().into_dart(),
self.swap_id.into_into_dart().into_dart(),
self.timestamp.into_into_dart().into_dart(), self.timestamp.into_into_dart().into_dart(),
self.amount_sat.into_into_dart().into_dart(), self.amount_sat.into_into_dart().into_dart(),
self.fees_sat.into_into_dart().into_dart(), self.fees_sat.into_into_dart().into_dart(),
self.payment_type.into_into_dart().into_dart(), self.payment_type.into_into_dart().into_dart(),
self.invoice.into_into_dart().into_dart(), self.status.into_into_dart().into_dart(),
] ]
.into_dart() .into_dart()
} }
@@ -715,10 +740,10 @@ impl flutter_rust_bridge::IntoDart for crate::error::PaymentError {
} }
crate::error::PaymentError::PairsNotFound => [8.into_dart()].into_dart(), crate::error::PaymentError::PairsNotFound => [8.into_dart()].into_dart(),
crate::error::PaymentError::PersistError => [9.into_dart()].into_dart(), crate::error::PaymentError::PersistError => [9.into_dart()].into_dart(),
crate::error::PaymentError::Refunded { err, txid } => [ crate::error::PaymentError::Refunded { err, refund_tx_id } => [
10.into_dart(), 10.into_dart(),
err.into_into_dart().into_dart(), err.into_into_dart().into_dart(),
txid.into_into_dart().into_dart(), refund_tx_id.into_into_dart().into_dart(),
] ]
.into_dart(), .into_dart(),
crate::error::PaymentError::SendError { err } => { crate::error::PaymentError::SendError { err } => {
@@ -740,10 +765,8 @@ impl flutter_rust_bridge::IntoIntoDart<crate::error::PaymentError> for crate::er
impl flutter_rust_bridge::IntoDart for crate::model::PaymentType { impl flutter_rust_bridge::IntoDart for crate::model::PaymentType {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self { match self {
Self::Sent => 0.into_dart(), Self::Receive => 0.into_dart(),
Self::Received => 1.into_dart(), Self::Send => 1.into_dart(),
Self::PendingReceive => 2.into_dart(),
Self::PendingSend => 3.into_dart(),
} }
} }
} }
@@ -923,6 +946,8 @@ impl SseEncode for crate::model::GetInfoResponse {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<u64>::sse_encode(self.balance_sat, serializer); <u64>::sse_encode(self.balance_sat, serializer);
<u64>::sse_encode(self.pending_send_sat, serializer);
<u64>::sse_encode(self.pending_receive_sat, serializer);
<String>::sse_encode(self.pubkey, serializer); <String>::sse_encode(self.pubkey, serializer);
} }
} }
@@ -970,6 +995,24 @@ impl SseEncode for crate::model::Network {
} }
} }
impl SseEncode for crate::model::PaymentState {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(
match self {
crate::model::PaymentState::Created => 0,
crate::model::PaymentState::Pending => 1,
crate::model::PaymentState::Complete => 2,
crate::model::PaymentState::Failed => 3,
_ => {
unimplemented!("");
}
},
serializer,
);
}
}
impl SseEncode for Option<String> { impl SseEncode for Option<String> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -980,16 +1023,6 @@ impl SseEncode for Option<String> {
} }
} }
impl SseEncode for Option<u32> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<u32>::sse_encode(value, serializer);
}
}
}
impl SseEncode for Option<u64> { impl SseEncode for Option<u64> {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -1003,12 +1036,13 @@ impl SseEncode for Option<u64> {
impl SseEncode for crate::model::Payment { impl SseEncode for crate::model::Payment {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<Option<String>>::sse_encode(self.id, serializer); <String>::sse_encode(self.tx_id, serializer);
<Option<u32>>::sse_encode(self.timestamp, serializer); <Option<String>>::sse_encode(self.swap_id, serializer);
<u32>::sse_encode(self.timestamp, serializer);
<u64>::sse_encode(self.amount_sat, serializer); <u64>::sse_encode(self.amount_sat, serializer);
<Option<u64>>::sse_encode(self.fees_sat, serializer); <Option<u64>>::sse_encode(self.fees_sat, serializer);
<crate::model::PaymentType>::sse_encode(self.payment_type, serializer); <crate::model::PaymentType>::sse_encode(self.payment_type, serializer);
<Option<String>>::sse_encode(self.invoice, serializer); <crate::model::PaymentState>::sse_encode(self.status, serializer);
} }
} }
@@ -1048,10 +1082,10 @@ impl SseEncode for crate::error::PaymentError {
crate::error::PaymentError::PersistError => { crate::error::PaymentError::PersistError => {
<i32>::sse_encode(9, serializer); <i32>::sse_encode(9, serializer);
} }
crate::error::PaymentError::Refunded { err, txid } => { crate::error::PaymentError::Refunded { err, refund_tx_id } => {
<i32>::sse_encode(10, serializer); <i32>::sse_encode(10, serializer);
<String>::sse_encode(err, serializer); <String>::sse_encode(err, serializer);
<String>::sse_encode(txid, serializer); <String>::sse_encode(refund_tx_id, serializer);
} }
crate::error::PaymentError::SendError { err } => { crate::error::PaymentError::SendError { err } => {
<i32>::sse_encode(11, serializer); <i32>::sse_encode(11, serializer);
@@ -1070,10 +1104,8 @@ impl SseEncode for crate::model::PaymentType {
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode( <i32>::sse_encode(
match self { match self {
crate::model::PaymentType::Sent => 0, crate::model::PaymentType::Receive => 0,
crate::model::PaymentType::Received => 1, crate::model::PaymentType::Send => 1,
crate::model::PaymentType::PendingReceive => 2,
crate::model::PaymentType::PendingSend => 3,
_ => { _ => {
unimplemented!(""); unimplemented!("");
} }

View File

@@ -1,11 +1,17 @@
use anyhow::anyhow; use anyhow::anyhow;
use boltz_client::network::Chain; use boltz_client::network::Chain;
use boltz_client::Bolt11Invoice; use boltz_client::swaps::boltzv2::{
CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree,
};
use boltz_client::SwapType;
use lwk_signer::SwSigner; use lwk_signer::SwSigner;
use lwk_wollet::{ElectrumUrl, ElementsNetwork, WolletDescriptor}; use lwk_wollet::{ElectrumUrl, ElementsNetwork, WolletDescriptor};
use serde::Serialize; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::ToSql;
use serde::{Deserialize, Serialize};
use crate::get_invoice_amount; use crate::error::PaymentError;
use crate::utils;
#[derive(Debug, Copy, Clone, PartialEq, Serialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize)]
pub enum Network { pub enum Network {
@@ -119,7 +125,12 @@ pub struct GetInfoRequest {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct GetInfoResponse { pub struct GetInfoResponse {
/// Usable balance. This is the confirmed onchain balance minus `pending_send_sat`.
pub balance_sat: u64, pub balance_sat: u64,
/// Amount that is being used for ongoing Send swaps
pub pending_send_sat: u64,
/// Incoming amount that is pending from ongoing Receive swaps
pub pending_receive_sat: u64,
pub pubkey: String, pub pubkey: String,
} }
@@ -129,99 +140,405 @@ pub struct RestoreRequest {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum OngoingSwap { pub(crate) enum Swap {
Send(OngoingSwapIn), Send(SwapIn),
Receive(OngoingSwapOut), Receive(SwapOut),
} }
impl OngoingSwap { impl Swap {
pub(crate) fn id(&self) -> String { pub(crate) fn id(&self) -> String {
match &self { match &self {
OngoingSwap::Send(OngoingSwapIn { id, .. }) Swap::Send(SwapIn { id, .. }) | Swap::Receive(SwapOut { id, .. }) => id.clone(),
| OngoingSwap::Receive(OngoingSwapOut { id, .. }) => id.clone(), }
}
pub(crate) fn swap_type(&self) -> SwapType {
match &self {
Swap::Send(_) => SwapType::Submarine,
Swap::Receive(_) => SwapType::ReverseSubmarine,
} }
} }
} }
/// A submarine swap, used for swap-in (Send)
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct OngoingSwapIn { pub(crate) struct SwapIn {
pub(crate) id: String, pub(crate) id: String,
pub(crate) invoice: String, pub(crate) invoice: String,
pub(crate) payer_amount_sat: u64, pub(crate) payer_amount_sat: u64,
pub(crate) receiver_amount_sat: u64,
/// JSON representation of [crate::persist::swap_in::InternalCreateSubmarineResponse]
pub(crate) create_response_json: String, pub(crate) create_response_json: String,
pub(crate) lockup_txid: Option<String>, /// Persisted only when the lockup tx is successfully broadcast
pub(crate) lockup_tx_id: Option<String>,
/// Persisted as soon as a refund tx is broadcast
pub(crate) refund_tx_id: Option<String>,
pub(crate) created_at: u32,
pub(crate) state: PaymentState,
}
impl SwapIn {
pub(crate) fn get_boltz_create_response(
&self,
) -> Result<CreateSubmarineResponse, PaymentError> {
let internal_create_response: crate::persist::swap_in::InternalCreateSubmarineResponse =
serde_json::from_str(&self.create_response_json).map_err(|e| {
PaymentError::Generic {
err: format!("Failed to deserialize InternalCreateSubmarineResponse: {e:?}"),
}
})?;
let res = CreateSubmarineResponse {
id: self.id.clone(),
accept_zero_conf: internal_create_response.accept_zero_conf,
address: internal_create_response.address.clone(),
bip21: internal_create_response.bip21.clone(),
claim_public_key: crate::utils::json_to_pubkey(
&internal_create_response.claim_public_key,
)?,
expected_amount: internal_create_response.expected_amount,
swap_tree: internal_create_response.swap_tree.clone().into(),
blinding_key: internal_create_response.blinding_key.clone(),
};
Ok(res)
}
pub(crate) fn from_boltz_struct_to_json(
create_response: &CreateSubmarineResponse,
expected_swap_id: &str,
) -> Result<String, PaymentError> {
let internal_create_response =
crate::persist::swap_in::InternalCreateSubmarineResponse::try_convert_from_boltz(
create_response,
expected_swap_id,
)?;
let create_response_json =
serde_json::to_string(&internal_create_response).map_err(|e| {
PaymentError::Generic {
err: format!("Failed to serialize InternalCreateSubmarineResponse: {e:?}"),
}
})?;
Ok(create_response_json)
}
} }
/// A reverse swap, used for swap-out (Receive)
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct OngoingSwapOut { pub(crate) struct SwapOut {
pub(crate) id: String, pub(crate) id: String,
pub(crate) preimage: String, pub(crate) preimage: String,
pub(crate) redeem_script: String, /// JSON representation of [crate::persist::swap_out::InternalCreateReverseResponse]
pub(crate) blinding_key: String, pub(crate) create_response_json: String,
pub(crate) invoice: String, pub(crate) invoice: String,
/// The amount of the invoice
pub(crate) payer_amount_sat: u64,
pub(crate) receiver_amount_sat: u64, pub(crate) receiver_amount_sat: u64,
pub(crate) claim_fees_sat: u64, pub(crate) claim_fees_sat: u64,
/// Persisted as soon as a claim tx is broadcast
pub(crate) claim_tx_id: Option<String>,
pub(crate) created_at: u32,
pub(crate) state: PaymentState,
} }
impl SwapOut {
#[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) fn get_boltz_create_response(&self) -> Result<CreateReverseResponse, PaymentError> {
pub enum PaymentType { let internal_create_response: crate::persist::swap_out::InternalCreateReverseResponse =
Sent, serde_json::from_str(&self.create_response_json).map_err(|e| {
Received, PaymentError::Generic {
PendingReceive, err: format!("Failed to deserialize InternalCreateReverseResponse: {e:?}"),
PendingSend,
}
#[derive(Debug, Clone, Serialize)]
pub struct Payment {
pub id: Option<String>,
pub timestamp: Option<u32>,
pub amount_sat: u64,
pub fees_sat: Option<u64>,
#[serde(rename(serialize = "type"))]
pub payment_type: PaymentType,
pub invoice: Option<String>,
}
impl From<OngoingSwap> for Payment {
fn from(swap: OngoingSwap) -> Self {
match swap {
OngoingSwap::Send(OngoingSwapIn {
invoice,
payer_amount_sat,
..
}) => {
let receiver_amount_sat = get_invoice_amount!(invoice);
Payment {
id: None,
timestamp: None,
payment_type: PaymentType::PendingSend,
amount_sat: payer_amount_sat,
invoice: Some(invoice),
fees_sat: Some(payer_amount_sat - receiver_amount_sat),
} }
} })?;
OngoingSwap::Receive(OngoingSwapOut {
receiver_amount_sat, let res = CreateReverseResponse {
invoice, id: self.id.clone(),
.. invoice: self.invoice.clone(),
}) => { swap_tree: internal_create_response.swap_tree.clone().into(),
let payer_amount_sat = get_invoice_amount!(invoice); lockup_address: internal_create_response.lockup_address.clone(),
Payment { refund_public_key: crate::utils::json_to_pubkey(
id: None, &internal_create_response.refund_public_key,
timestamp: None, )?,
payment_type: PaymentType::PendingReceive, timeout_block_height: internal_create_response.timeout_block_height,
amount_sat: receiver_amount_sat, onchain_amount: internal_create_response.onchain_amount,
invoice: Some(invoice), blinding_key: internal_create_response.blinding_key.clone(),
fees_sat: Some(payer_amount_sat - receiver_amount_sat), };
Ok(res)
}
pub(crate) fn from_boltz_struct_to_json(
create_response: &CreateReverseResponse,
expected_swap_id: &str,
expected_invoice: &str,
) -> Result<String, PaymentError> {
let internal_create_response =
crate::persist::swap_out::InternalCreateReverseResponse::try_convert_from_boltz(
create_response,
expected_swap_id,
expected_invoice,
)?;
let create_response_json =
serde_json::to_string(&internal_create_response).map_err(|e| {
PaymentError::Generic {
err: format!("Failed to serialize InternalCreateReverseResponse: {e:?}"),
} }
} })?;
Ok(create_response_json)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
pub enum PaymentState {
Created = 0,
/// ## Receive Swaps
///
/// Covers the cases when
/// - the lockup tx is seen in the mempool or
/// - our claim tx is broadcast
///
/// When the claim tx is broadcast, `claim_tx_id` is set in the swap.
///
/// ## Send Swaps
///
/// Covers the cases when
/// - our lockup tx was broadcast or
/// - a refund was initiated and our refund tx was broadcast
///
/// When the refund tx is broadcast, `refund_tx_id` is set in the swap.
///
/// ## No swap data available
///
/// If no associated swap is found, this indicates the underlying tx is not confirmed yet.
Pending = 1,
/// ## Receive Swaps
///
/// Covers the case when the claim tx is confirmed.
///
/// ## Send Swaps
///
/// This is the status when the claim tx is broadcast and we see it in the mempool.
///
/// ## No swap data available
///
/// If no associated swap is found, this indicates the underlying tx is confirmed.
Complete = 2,
/// ## Receive Swaps
///
/// This is the status when the swap failed for any reason and the Receive could not complete.
///
/// ## Send Swaps
///
/// This is the status when a swap refund was initiated and the refund tx is confirmed.
Failed = 3,
}
impl ToSql for PaymentState {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
}
}
impl FromSql for PaymentState {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
match value {
ValueRef::Integer(i) => match i as u8 {
0 => Ok(PaymentState::Created),
1 => Ok(PaymentState::Pending),
2 => Ok(PaymentState::Complete),
3 => Ok(PaymentState::Failed),
_ => Err(FromSqlError::OutOfRange(i)),
},
_ => Err(FromSqlError::InvalidType),
} }
} }
} }
pub(crate) struct PaymentData { #[derive(Debug, Copy, Clone, PartialEq, Serialize)]
pub enum PaymentType {
Receive = 0,
Send = 1,
}
impl ToSql for PaymentType {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
}
}
impl FromSql for PaymentType {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
match value {
ValueRef::Integer(i) => match i as u8 {
0 => Ok(PaymentType::Receive),
1 => Ok(PaymentType::Send),
_ => Err(FromSqlError::OutOfRange(i)),
},
_ => Err(FromSqlError::InvalidType),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub enum PaymentStatus {
Pending = 0,
Complete = 1,
}
impl ToSql for PaymentStatus {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::from(*self as i8))
}
}
impl FromSql for PaymentStatus {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
match value {
ValueRef::Integer(i) => match i as u8 {
0 => Ok(PaymentStatus::Pending),
1 => Ok(PaymentStatus::Complete),
_ => Err(FromSqlError::OutOfRange(i)),
},
_ => Err(FromSqlError::InvalidType),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PaymentTxData {
/// The tx ID of the transaction
pub tx_id: String,
/// The point in time when the underlying tx was included in a block.
pub timestamp: Option<u32>,
/// The onchain tx amount.
///
/// In case of an outbound payment (Send), this is the payer amount. Otherwise it's the receiver amount.
pub amount_sat: u64,
pub payment_type: PaymentType,
/// Onchain tx status
pub is_confirmed: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct PaymentSwapData {
pub swap_id: String,
/// Swap creation timestamp
pub created_at: u32,
/// Amount sent by the swap payer
pub payer_amount_sat: u64, pub payer_amount_sat: u64,
/// Amount received by the swap receiver
pub receiver_amount_sat: u64, pub receiver_amount_sat: u64,
/// Payment status derived from the swap status
pub status: PaymentState,
}
/// Represents an SDK payment.
///
/// By default, this is an onchain tx. It may represent a swap, if swap metadata is available.
#[derive(Debug, Clone, Serialize)]
pub struct Payment {
/// The tx ID of the onchain transaction
pub tx_id: String,
/// The swap ID, if any swap is associated with this payment
pub swap_id: Option<String>,
/// Composite timestamp that can be used for sorting or displaying the payment.
///
/// If this payment has an associated swap, it is the swap creation time. Otherwise, the point
/// in time when the underlying tx was included in a block. If there is no associated swap
/// available and the underlying tx is not yet confirmed, the value is `now()`.
pub timestamp: u32,
/// The payment amount, which corresponds to the onchain tx amount.
///
/// In case of an outbound payment (Send), this is the payer amount. Otherwise it's the receiver amount.
pub amount_sat: u64,
/// If a swap is associated with this payment, this represents the total fees paid by the
/// sender. In other words, it's the delta between the amount that was sent and the amount
/// received.
pub fees_sat: Option<u64>,
pub payment_type: PaymentType,
/// Composite status representing the overall status of the payment.
///
/// If the tx has no associated swap, this reflects the onchain tx status (confirmed or not).
///
/// If the tx has an associated swap, this is determined by the swap status (pending or complete).
pub status: PaymentState,
}
impl Payment {
pub(crate) fn from(tx: PaymentTxData, swap: Option<PaymentSwapData>) -> Payment {
Payment {
tx_id: tx.tx_id,
swap_id: swap.as_ref().map(|s| s.swap_id.clone()),
timestamp: match swap {
Some(ref swap) => swap.created_at,
None => tx.timestamp.unwrap_or(utils::now()),
},
amount_sat: tx.amount_sat,
fees_sat: swap
.as_ref()
.map(|s| s.payer_amount_sat - s.receiver_amount_sat),
payment_type: tx.payment_type,
status: match swap {
Some(swap) => swap.status,
None => match tx.is_confirmed {
true => PaymentState::Complete,
false => PaymentState::Pending,
},
},
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct InternalLeaf {
pub output: String,
pub version: u8,
}
impl From<InternalLeaf> for Leaf {
fn from(value: InternalLeaf) -> Self {
Leaf {
output: value.output,
version: value.version,
}
}
}
impl From<Leaf> for InternalLeaf {
fn from(value: Leaf) -> Self {
InternalLeaf {
output: value.output,
version: value.version,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(super) struct InternalSwapTree {
claim_leaf: InternalLeaf,
refund_leaf: InternalLeaf,
}
impl From<InternalSwapTree> for SwapTree {
fn from(value: InternalSwapTree) -> Self {
SwapTree {
claim_leaf: value.claim_leaf.into(),
refund_leaf: value.refund_leaf.into(),
}
}
}
impl From<SwapTree> for InternalSwapTree {
fn from(value: SwapTree) -> Self {
InternalSwapTree {
claim_leaf: value.claim_leaf.into(),
refund_leaf: value.refund_leaf.into(),
}
}
} }
#[macro_export] #[macro_export]

View File

@@ -1,27 +1,34 @@
pub(crate) fn current_migrations() -> Vec<&'static str> { pub(crate) fn current_migrations() -> Vec<&'static str> {
vec![ vec![
"CREATE TABLE IF NOT EXISTS ongoing_receive_swaps ( "CREATE TABLE IF NOT EXISTS receive_swaps (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
preimage TEXT NOT NULL, preimage TEXT NOT NULL,
redeem_script TEXT NOT NULL, create_response_json TEXT NOT NULL,
blinding_key TEXT NOT NULL,
invoice TEXT NOT NULL, invoice TEXT NOT NULL,
payer_amount_sat INTEGER NOT NULL,
receiver_amount_sat INTEGER NOT NULL, receiver_amount_sat INTEGER NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP, created_at INTEGER NOT NULL,
claim_fees_sat INTEGER NOT NULL claim_fees_sat INTEGER NOT NULL,
claim_tx_id TEXT,
state INTEGER NOT NULL
) STRICT;", ) STRICT;",
"CREATE TABLE IF NOT EXISTS ongoing_send_swaps( "CREATE TABLE IF NOT EXISTS send_swaps (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
invoice TEXT NOT NULL, invoice TEXT NOT NULL,
payer_amount_sat INTEGER NOT NULL, payer_amount_sat INTEGER NOT NULL,
receiver_amount_sat INTEGER NOT NULL,
create_response_json TEXT NOT NULL, create_response_json TEXT NOT NULL,
lockup_txid TEXT, lockup_tx_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP refund_tx_id TEXT,
) STRICT;", created_at INTEGER NOT NULL,
"CREATE TABLE IF NOT EXISTS payment_data( state INTEGER NOT NULL
id TEXT NOT NULL PRIMARY KEY, ) STRICT;",
payer_amount_sat INTEGER NOT NULL, "CREATE TABLE IF NOT EXISTS payment_tx_data (
receiver_amount_sat INTEGER NOT NULL tx_id TEXT NOT NULL PRIMARY KEY,
payment_type INTEGER NOT NULL,
is_confirmed INTEGER NOT NULL,
timestamp INTEGER,
amount_sat INTEGER NOT NULL
) STRICT;", ) STRICT;",
] ]
} }

View File

@@ -1,7 +1,7 @@
mod backup; mod backup;
mod migrations; mod migrations;
mod swap_in; pub(crate) mod swap_in;
mod swap_out; pub(crate) mod swap_out;
use std::{collections::HashMap, fs::create_dir_all, path::PathBuf, str::FromStr}; use std::{collections::HashMap, fs::create_dir_all, path::PathBuf, str::FromStr};
@@ -11,6 +11,7 @@ use rusqlite::{params, Connection};
use rusqlite_migration::{Migrations, M}; use rusqlite_migration::{Migrations, M};
use crate::model::{Network::*, *}; use crate::model::{Network::*, *};
use crate::utils;
pub(crate) struct Persister { pub(crate) struct Persister {
main_db_dir: PathBuf, main_db_dir: PathBuf,
@@ -49,69 +50,118 @@ impl Persister {
Ok(()) Ok(())
} }
pub fn resolve_ongoing_swap( pub(crate) fn insert_or_update_payment(&self, ptx: PaymentTxData) -> Result<()> {
&self,
id: &str,
payment_data: Option<(String, PaymentData)>,
) -> Result<()> {
let mut con = self.get_connection()?; let mut con = self.get_connection()?;
let tx = con.transaction()?; let tx = con.transaction()?;
tx.execute("DELETE FROM ongoing_send_swaps WHERE id = ?", params![id])?;
tx.execute( tx.execute(
"DELETE FROM ongoing_receive_swaps WHERE id = ?", "INSERT OR REPLACE INTO payment_tx_data (
params![id], tx_id,
timestamp,
amount_sat,
payment_type,
is_confirmed
)
VALUES (?, ?, ?, ?, ?)
",
(
ptx.tx_id,
ptx.timestamp,
ptx.amount_sat,
ptx.payment_type,
ptx.is_confirmed,
),
)?; )?;
if let Some((txid, payment_data)) = payment_data {
tx.execute(
"INSERT INTO payment_data(id, payer_amount_sat, receiver_amount_sat)
VALUES(?, ?, ?)",
(
txid,
payment_data.payer_amount_sat,
payment_data.receiver_amount_sat,
),
)?;
}
tx.commit()?; tx.commit()?;
Ok(()) Ok(())
} }
pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<OngoingSwap>> { pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
let con = self.get_connection()?; let con = self.get_connection()?;
let ongoing_swap_ins: Vec<OngoingSwap> = self let ongoing_swap_ins: Vec<Swap> = self
.list_ongoing_send(&con, vec![])? .list_ongoing_send_swaps(&con)?
.into_iter() .into_iter()
.map(OngoingSwap::Send) .map(Swap::Send)
.collect(); .collect();
let ongoing_swap_outs: Vec<OngoingSwap> = self let ongoing_swap_outs: Vec<Swap> = self
.list_ongoing_receive(&con, vec![])? .list_ongoing_receive_swaps(&con)?
.into_iter() .into_iter()
.map(OngoingSwap::Receive) .map(Swap::Receive)
.collect(); .collect();
Ok([ongoing_swap_ins, ongoing_swap_outs].concat()) Ok([ongoing_swap_ins, ongoing_swap_outs].concat())
} }
pub fn get_payment_data(&self) -> Result<HashMap<String, PaymentData>> { pub fn get_payments(&self) -> Result<HashMap<String, Payment>> {
let con = self.get_connection()?; let con = self.get_connection()?;
// TODO For refund txs, do not create a new Payment
// Assumes there is no swap chaining (send swap lockup tx = receive swap claim tx)
let mut stmt = con.prepare( let mut stmt = con.prepare(
" "
SELECT id, payer_amount_sat, receiver_amount_sat SELECT
FROM payment_data ptx.tx_id,
ptx.timestamp,
ptx.amount_sat,
ptx.payment_type,
ptx.is_confirmed,
rs.id,
rs.created_at,
rs.payer_amount_sat,
rs.receiver_amount_sat,
rs.state,
ss.id,
ss.created_at,
ss.payer_amount_sat,
ss.receiver_amount_sat,
ss.state
FROM payment_tx_data AS ptx
LEFT JOIN receive_swaps AS rs
ON ptx.tx_id = rs.claim_tx_id
LEFT JOIN send_swaps AS ss
ON ptx.tx_id = ss.lockup_tx_id
", ",
)?; )?;
let data = stmt let data = stmt
.query_map(params![], |row| { .query_map(params![], |row| {
Ok(( let tx = PaymentTxData {
row.get(0)?, tx_id: row.get(0)?,
PaymentData { timestamp: row.get(1)?,
payer_amount_sat: row.get(1)?, amount_sat: row.get(2)?,
receiver_amount_sat: row.get(2)?, payment_type: row.get(3)?,
}, is_confirmed: row.get(4)?,
)) };
let maybe_receive_swap_id: Option<String> = row.get(5)?;
let maybe_receive_swap_created_at: Option<u32> = row.get(6)?;
let maybe_receive_swap_payer_amount_sat: Option<u64> = row.get(7)?;
let maybe_receive_swap_receiver_amount_sat: Option<u64> = row.get(8)?;
let maybe_receive_swap_receiver_state: Option<PaymentState> = row.get(9)?;
let maybe_send_swap_id: Option<String> = row.get(10)?;
let maybe_send_swap_created_at: Option<u32> = row.get(11)?;
let maybe_send_swap_payer_amount_sat: Option<u64> = row.get(12)?;
let maybe_send_swap_receiver_amount_sat: Option<u64> = row.get(13)?;
let maybe_send_swap_state: Option<PaymentState> = row.get(14)?;
let swap = match maybe_receive_swap_id {
Some(receive_swap_id) => Some(PaymentSwapData {
swap_id: receive_swap_id,
created_at: maybe_receive_swap_created_at.unwrap_or(utils::now()),
payer_amount_sat: maybe_receive_swap_payer_amount_sat.unwrap_or(0),
receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0),
status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created),
}),
None => maybe_send_swap_id.map(|send_swap_id| PaymentSwapData {
swap_id: send_swap_id,
created_at: maybe_send_swap_created_at.unwrap_or(utils::now()),
payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0),
receiver_amount_sat: maybe_send_swap_receiver_amount_sat.unwrap_or(0),
status: maybe_send_swap_state.unwrap_or(PaymentState::Created),
}),
};
Ok((tx.tx_id.clone(), Payment::from(tx, swap)))
})? })?
.map(|i| i.unwrap()) .map(|i| i.unwrap())
.collect(); .collect();

View File

@@ -1,36 +1,50 @@
use std::collections::HashMap;
use anyhow::Result;
use boltz_client::swaps::boltzv2::CreateSubmarineResponse;
use rusqlite::{named_params, params, Connection, OptionalExtension, Row};
use serde::{Deserialize, Serialize};
use crate::ensure_sdk;
use crate::error::PaymentError;
use crate::model::*; use crate::model::*;
use crate::persist::Persister; use crate::persist::Persister;
use anyhow::Result;
use rusqlite::{params, Connection, OptionalExtension, Row};
impl Persister { impl Persister {
pub(crate) fn insert_or_update_ongoing_swap_in(&self, swap_in: OngoingSwapIn) -> Result<()> { pub(crate) fn insert_swap_in(&self, swap_in: SwapIn) -> Result<()> {
let con = self.get_connection()?; let con = self.get_connection()?;
let mut stmt = con.prepare( let mut stmt = con.prepare(
" "
INSERT OR REPLACE INTO ongoing_send_swaps ( INSERT INTO send_swaps (
id, id,
invoice, invoice,
payer_amount_sat, payer_amount_sat,
receiver_amount_sat,
create_response_json, create_response_json,
lockup_txid lockup_tx_id,
refund_tx_id,
created_at,
state
) )
VALUES (?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
)?; )?;
_ = stmt.execute(( _ = stmt.execute((
swap_in.id, swap_in.id,
swap_in.invoice, swap_in.invoice,
swap_in.payer_amount_sat, swap_in.payer_amount_sat,
swap_in.receiver_amount_sat,
swap_in.create_response_json, swap_in.create_response_json,
swap_in.lockup_txid, swap_in.lockup_tx_id,
swap_in.refund_tx_id,
swap_in.created_at,
swap_in.state,
))?; ))?;
Ok(()) Ok(())
} }
fn list_ongoing_swap_in_query(where_clauses: Vec<&str>) -> String { fn list_swap_in_query(where_clauses: Vec<String>) -> String {
let mut where_clause_str = String::new(); let mut where_clause_str = String::new();
if !where_clauses.is_empty() { if !where_clauses.is_empty() {
where_clause_str = String::from("WHERE "); where_clause_str = String::from("WHERE ");
@@ -43,46 +57,172 @@ impl Persister {
id, id,
invoice, invoice,
payer_amount_sat, payer_amount_sat,
receiver_amount_sat,
create_response_json, create_response_json,
lockup_txid, lockup_tx_id,
created_at refund_tx_id,
FROM ongoing_send_swaps created_at,
state
FROM send_swaps
{where_clause_str} {where_clause_str}
ORDER BY created_at ORDER BY created_at
" "
) )
} }
pub(crate) fn fetch_ongoing_swap_in( pub(crate) fn fetch_swap_in(con: &Connection, id: &str) -> rusqlite::Result<Option<SwapIn>> {
con: &Connection, let query = Self::list_swap_in_query(vec!["id = ?1".to_string()]);
id: &str, con.query_row(&query, [id], Self::sql_row_to_swap_in)
) -> rusqlite::Result<Option<OngoingSwapIn>> {
let query = Self::list_ongoing_swap_in_query(vec!["id = ?1"]);
con.query_row(&query, [id], Self::sql_row_to_ongoing_swap_in)
.optional() .optional()
} }
fn sql_row_to_ongoing_swap_in(row: &Row) -> rusqlite::Result<OngoingSwapIn> { fn sql_row_to_swap_in(row: &Row) -> rusqlite::Result<SwapIn> {
Ok(OngoingSwapIn { Ok(SwapIn {
id: row.get(0)?, id: row.get(0)?,
invoice: row.get(1)?, invoice: row.get(1)?,
payer_amount_sat: row.get(2)?, payer_amount_sat: row.get(2)?,
create_response_json: row.get(3)?, receiver_amount_sat: row.get(3)?,
lockup_txid: row.get(4)?, create_response_json: row.get(4)?,
lockup_tx_id: row.get(5)?,
refund_tx_id: row.get(6)?,
created_at: row.get(7)?,
state: row.get(8)?,
}) })
} }
pub(crate) fn list_ongoing_send( pub(crate) fn list_send_swaps(
&self, &self,
con: &Connection, con: &Connection,
where_clauses: Vec<&str>, where_clauses: Vec<String>,
) -> rusqlite::Result<Vec<OngoingSwapIn>> { ) -> rusqlite::Result<Vec<SwapIn>> {
let query = Self::list_ongoing_swap_in_query(where_clauses); let query = Self::list_swap_in_query(where_clauses);
let ongoing_send = con let ongoing_send = con
.prepare(&query)? .prepare(&query)?
.query_map(params![], Self::sql_row_to_ongoing_swap_in)? .query_map(params![], Self::sql_row_to_swap_in)?
.map(|i| i.unwrap()) .map(|i| i.unwrap())
.collect(); .collect();
Ok(ongoing_send) Ok(ongoing_send)
} }
pub(crate) fn list_ongoing_send_swaps(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<SwapIn>> {
let mut where_clause: Vec<String> = Vec::new();
where_clause.push(format!(
"state in ({})",
[PaymentState::Created, PaymentState::Pending]
.iter()
.map(|t| format!("'{}'", *t as i8))
.collect::<Vec<_>>()
.join(", ")
));
self.list_send_swaps(con, where_clause)
}
pub(crate) fn list_pending_send_swaps(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<SwapIn>> {
let query = Self::list_swap_in_query(vec!["state = ?1".to_string()]);
let res = con
.prepare(&query)?
.query_map(params![PaymentState::Pending], Self::sql_row_to_swap_in)?
.map(|i| i.unwrap())
.collect();
Ok(res)
}
/// Pending swap ins, indexed by refund tx id
pub(crate) fn list_pending_send_swaps_by_refund_tx_id(
&self,
con: &Connection,
) -> rusqlite::Result<HashMap<String, SwapIn>> {
let res: HashMap<String, SwapIn> = self
.list_pending_send_swaps(con)?
.iter()
.filter_map(|pending_swap_in| {
pending_swap_in
.refund_tx_id
.as_ref()
.map(|refund_tx_id| (refund_tx_id.clone(), pending_swap_in.clone()))
})
.collect();
Ok(res)
}
pub(crate) fn try_handle_send_swap_update(
&self,
con: &Connection,
swap_id: &str,
to_state: PaymentState,
lockup_tx_id: Option<&str>,
refund_tx_id: Option<&str>,
) -> Result<(), PaymentError> {
// Do not overwrite lockup_tx_id, refund_tx_id
con.execute(
"UPDATE send_swaps
SET
lockup_tx_id =
CASE
WHEN lockup_tx_id IS NULL THEN :lockup_tx_id
ELSE lockup_tx_id
END,
refund_tx_id =
CASE
WHEN refund_tx_id IS NULL THEN :refund_tx_id
ELSE refund_tx_id
END,
state=:state
WHERE
id = :id",
named_params! {
":id": swap_id,
":lockup_tx_id": lockup_tx_id,
":refund_tx_id": refund_tx_id,
":state": to_state,
},
)
.map_err(|_| PaymentError::PersistError)?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct InternalCreateSubmarineResponse {
pub(crate) accept_zero_conf: bool,
pub(crate) address: String,
pub(crate) bip21: String,
pub(crate) claim_public_key: String,
pub(crate) expected_amount: u64,
pub(crate) swap_tree: InternalSwapTree,
pub(crate) blinding_key: Option<String>,
}
impl InternalCreateSubmarineResponse {
pub(crate) fn try_convert_from_boltz(
boltz_create_response: &CreateSubmarineResponse,
expected_swap_id: &str,
) -> Result<InternalCreateSubmarineResponse, PaymentError> {
// Do not store the CreateResponse fields that are already stored separately
// Before skipping them, ensure they match the separately stored ones
ensure_sdk!(
boltz_create_response.id == expected_swap_id,
PaymentError::PersistError
);
let res = InternalCreateSubmarineResponse {
accept_zero_conf: boltz_create_response.accept_zero_conf,
address: boltz_create_response.address.clone(),
bip21: boltz_create_response.bip21.clone(),
claim_public_key: boltz_create_response.claim_public_key.to_string(),
expected_amount: boltz_create_response.expected_amount,
swap_tree: boltz_create_response.swap_tree.clone().into(),
blinding_key: boltz_create_response.blinding_key.clone(),
};
Ok(res)
}
} }

View File

@@ -1,40 +1,52 @@
use std::collections::HashMap;
use crate::ensure_sdk;
use crate::error::PaymentError;
use crate::model::*; use crate::model::*;
use crate::persist::Persister; use crate::persist::Persister;
use anyhow::Result; use anyhow::Result;
use rusqlite::{params, Connection, OptionalExtension, Row}; use boltz_client::swaps::boltzv2::CreateReverseResponse;
use rusqlite::{named_params, params, Connection, OptionalExtension, Row};
use serde::{Deserialize, Serialize};
impl Persister { impl Persister {
pub(crate) fn insert_or_update_ongoing_swap_out(&self, swap_out: OngoingSwapOut) -> Result<()> { pub(crate) fn insert_swap_out(&self, swap_out: SwapOut) -> Result<()> {
let con = self.get_connection()?; let con = self.get_connection()?;
let mut stmt = con.prepare( let mut stmt = con.prepare(
" "
INSERT OR REPLACE INTO ongoing_receive_swaps ( INSERT INTO receive_swaps (
id, id,
preimage, preimage,
redeem_script, create_response_json,
blinding_key,
invoice, invoice,
payer_amount_sat,
receiver_amount_sat, receiver_amount_sat,
claim_fees_sat created_at,
claim_fees_sat,
claim_tx_id,
state
) )
VALUES (?, ?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)?; )?;
_ = stmt.execute(( _ = stmt.execute((
swap_out.id, swap_out.id,
swap_out.preimage, swap_out.preimage,
swap_out.redeem_script, swap_out.create_response_json,
swap_out.blinding_key,
swap_out.invoice, swap_out.invoice,
swap_out.payer_amount_sat,
swap_out.receiver_amount_sat, swap_out.receiver_amount_sat,
swap_out.created_at,
swap_out.claim_fees_sat, swap_out.claim_fees_sat,
swap_out.claim_tx_id,
swap_out.state,
))?; ))?;
Ok(()) Ok(())
} }
fn list_ongoing_swap_out_query(where_clauses: Vec<&str>) -> String { fn list_swap_out_query(where_clauses: Vec<String>) -> String {
let mut where_clause_str = String::new(); let mut where_clause_str = String::new();
if !where_clauses.is_empty() { if !where_clauses.is_empty() {
where_clause_str = String::from("WHERE "); where_clause_str = String::from("WHERE ");
@@ -44,53 +56,172 @@ impl Persister {
format!( format!(
" "
SELECT SELECT
id, rs.id,
preimage, rs.preimage,
redeem_script, rs.create_response_json,
blinding_key, rs.invoice,
invoice, rs.payer_amount_sat,
receiver_amount_sat, rs.receiver_amount_sat,
claim_fees_sat, rs.claim_fees_sat,
created_at rs.claim_tx_id,
FROM ongoing_receive_swaps rs.created_at,
rs.state
FROM receive_swaps AS rs
{where_clause_str} {where_clause_str}
ORDER BY created_at ORDER BY rs.created_at
" "
) )
} }
pub(crate) fn fetch_ongoing_swap_out( pub(crate) fn fetch_swap_out(con: &Connection, id: &str) -> rusqlite::Result<Option<SwapOut>> {
con: &Connection, let query = Self::list_swap_out_query(vec!["id = ?1".to_string()]);
id: &str, con.query_row(&query, [id], Self::sql_row_to_swap_out)
) -> rusqlite::Result<Option<OngoingSwapOut>> {
let query = Self::list_ongoing_swap_out_query(vec!["id = ?1"]);
con.query_row(&query, [id], Self::sql_row_to_ongoing_swap_out)
.optional() .optional()
} }
fn sql_row_to_ongoing_swap_out(row: &Row) -> rusqlite::Result<OngoingSwapOut> { fn sql_row_to_swap_out(row: &Row) -> rusqlite::Result<SwapOut> {
Ok(OngoingSwapOut { Ok(SwapOut {
id: row.get(0)?, id: row.get(0)?,
preimage: row.get(1)?, preimage: row.get(1)?,
redeem_script: row.get(2)?, create_response_json: row.get(2)?,
blinding_key: row.get(3)?, invoice: row.get(3)?,
invoice: row.get(4)?, payer_amount_sat: row.get(4)?,
receiver_amount_sat: row.get(5)?, receiver_amount_sat: row.get(5)?,
claim_fees_sat: row.get(6)?, claim_fees_sat: row.get(6)?,
claim_tx_id: row.get(7)?,
created_at: row.get(8)?,
state: row.get(9)?,
}) })
} }
pub(crate) fn list_ongoing_receive( pub(crate) fn list_receive_swaps(
&self, &self,
con: &Connection, con: &Connection,
where_clauses: Vec<&str>, where_clauses: Vec<String>,
) -> rusqlite::Result<Vec<OngoingSwapOut>> { ) -> rusqlite::Result<Vec<SwapOut>> {
let query = Self::list_ongoing_swap_out_query(where_clauses); let query = Self::list_swap_out_query(where_clauses);
let ongoing_receive = con let ongoing_receive = con
.prepare(&query)? .prepare(&query)?
.query_map(params![], Self::sql_row_to_ongoing_swap_out)? .query_map(params![], Self::sql_row_to_swap_out)?
.map(|i| i.unwrap()) .map(|i| i.unwrap())
.collect(); .collect();
Ok(ongoing_receive) Ok(ongoing_receive)
} }
pub(crate) fn list_ongoing_receive_swaps(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<SwapOut>> {
let mut where_clause: Vec<String> = Vec::new();
where_clause.push(format!(
"state in ({})",
[PaymentState::Created, PaymentState::Pending]
.iter()
.map(|t| format!("'{}'", *t as i8))
.collect::<Vec<_>>()
.join(", ")
));
self.list_receive_swaps(con, where_clause)
}
pub(crate) fn list_pending_receive_swaps(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<SwapOut>> {
let query = Self::list_swap_out_query(vec!["state = ?1".to_string()]);
let res = con
.prepare(&query)?
.query_map(params![PaymentState::Pending], Self::sql_row_to_swap_out)?
.map(|i| i.unwrap())
.collect();
Ok(res)
}
/// Pending swap outs, indexed by claim_tx_id
pub(crate) fn list_pending_receive_swaps_by_claim_tx_id(
&self,
con: &Connection,
) -> rusqlite::Result<HashMap<String, SwapOut>> {
let res = self
.list_pending_receive_swaps(con)?
.iter()
.filter_map(|pending_swap_out| {
pending_swap_out
.claim_tx_id
.as_ref()
.map(|claim_tx_id| (claim_tx_id.clone(), pending_swap_out.clone()))
})
.collect();
Ok(res)
}
pub(crate) fn try_handle_receive_swap_update(
&self,
con: &Connection,
swap_id: &str,
to_state: PaymentState,
claim_tx_id: Option<&str>,
) -> Result<(), PaymentError> {
// Do not overwrite claim_tx_id
con.execute(
"UPDATE receive_swaps
SET
claim_tx_id =
CASE
WHEN claim_tx_id IS NULL THEN :claim_tx_id
ELSE claim_tx_id
END,
state = :state
WHERE
id = :id",
named_params! {
":id": swap_id,
":claim_tx_id": claim_tx_id,
":state": to_state,
},
)
.map_err(|_| PaymentError::PersistError)?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct InternalCreateReverseResponse {
pub swap_tree: InternalSwapTree,
pub lockup_address: String,
pub refund_public_key: String,
pub timeout_block_height: u32,
pub onchain_amount: u32,
pub blinding_key: Option<String>,
}
impl InternalCreateReverseResponse {
pub(crate) fn try_convert_from_boltz(
boltz_create_response: &CreateReverseResponse,
expected_swap_id: &str,
expected_invoice: &str,
) -> Result<Self, PaymentError> {
// Do not store the CreateResponse fields that are already stored separately
// Before skipping them, ensure they match the separately stored ones
ensure_sdk!(
boltz_create_response.id == expected_swap_id,
PaymentError::PersistError
);
ensure_sdk!(
boltz_create_response.invoice == expected_invoice,
PaymentError::PersistError
);
let res = InternalCreateReverseResponse {
swap_tree: boltz_create_response.swap_tree.clone().into(),
lockup_address: boltz_create_response.lockup_address.clone(),
refund_public_key: boltz_create_response.refund_public_key.to_string(),
timeout_block_height: boltz_create_response.timeout_block_height,
onchain_amount: boltz_create_response.onchain_amount,
blinding_key: boltz_create_response.blinding_key.clone(),
};
Ok(res)
}
} }

View File

@@ -1,9 +1,11 @@
use std::collections::HashMap;
use std::time::Instant;
use std::{ use std::{
fs, fs,
path::PathBuf, path::PathBuf,
str::FromStr, str::FromStr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::sleep, thread,
time::Duration, time::Duration,
}; };
@@ -16,18 +18,19 @@ use boltz_client::{
liquidv2::LBtcSwapTxV2, liquidv2::LBtcSwapTxV2,
}, },
util::secrets::{LiquidSwapKey, Preimage, SwapKey}, util::secrets::{LiquidSwapKey, Preimage, SwapKey},
Amount, Bolt11Invoice, Keypair, LBtcSwapScriptV2, SwapType, Amount, Bolt11Invoice, ElementsAddress, Keypair, LBtcSwapScriptV2, SwapType,
}; };
use elements::hashes::hex::DisplayHex; use log::{debug, error, info, warn};
use log::{debug, info, warn};
use lwk_common::{singlesig_desc, Signer, Singlesig}; use lwk_common::{singlesig_desc, Signer, Singlesig};
use lwk_signer::{AnySigner, SwSigner}; use lwk_signer::{AnySigner, SwSigner};
use lwk_wollet::{ use lwk_wollet::{
elements::{Address, Transaction}, elements::{Address, Transaction},
full_scan_with_electrum_client, BlockchainBackend, ElectrumClient, ElectrumUrl, BlockchainBackend, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister,
ElementsNetwork, FsPersister, Wollet as LwkWollet, WolletDescriptor, Wollet as LwkWollet, WolletDescriptor,
}; };
use crate::boltz_status_stream::set_stream_nonblocking;
use crate::model::PaymentState::*;
use crate::{ use crate::{
boltz_status_stream::BoltzStatusStream, ensure_sdk, error::PaymentError, get_invoice_amount, boltz_status_stream::BoltzStatusStream, ensure_sdk, error::PaymentError, get_invoice_amount,
model::*, persist::Persister, utils, model::*, persist::Persister, utils,
@@ -46,9 +49,7 @@ pub struct LiquidSdk {
lwk_wollet: Arc<Mutex<LwkWollet>>, lwk_wollet: Arc<Mutex<LwkWollet>>,
/// LWK Signer, for signing Liquid transactions /// LWK Signer, for signing Liquid transactions
lwk_signer: SwSigner, lwk_signer: SwSigner,
active_address: Option<u32>,
persister: Persister, persister: Persister,
status_stream: BoltzStatusStream,
data_dir_path: String, data_dir_path: String,
} }
@@ -58,13 +59,27 @@ impl LiquidSdk {
let signer = SwSigner::new(&req.mnemonic, is_mainnet)?; let signer = SwSigner::new(&req.mnemonic, is_mainnet)?;
let descriptor = LiquidSdk::get_descriptor(&signer, req.network)?; let descriptor = LiquidSdk::get_descriptor(&signer, req.network)?;
LiquidSdk::new(LiquidSdkOptions { let sdk = LiquidSdk::new(LiquidSdkOptions {
signer, signer,
descriptor, descriptor,
electrum_url: None, electrum_url: None,
data_dir_path: req.data_dir, data_dir_path: req.data_dir,
network: req.network, network: req.network,
}) })?;
BoltzStatusStream::track_pending_swaps(sdk.clone())?;
// Periodically run sync() in the background
let sdk_clone = sdk.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(30));
_ = sdk_clone.sync();
});
// Initial sync() before returning the instance
sdk.sync()?;
Ok(sdk)
} }
fn new(opts: LiquidSdkOptions) -> Result<Arc<Self>> { fn new(opts: LiquidSdkOptions) -> Result<Arc<Self>> {
@@ -85,21 +100,15 @@ impl LiquidSdk {
let persister = Persister::new(&data_dir_path, network)?; let persister = Persister::new(&data_dir_path, network)?;
persister.init()?; persister.init()?;
let status_stream = BoltzStatusStream::new();
let sdk = Arc::new(LiquidSdk { let sdk = Arc::new(LiquidSdk {
lwk_wollet, lwk_wollet,
network, network,
electrum_url, electrum_url,
lwk_signer: opts.signer, lwk_signer: opts.signer,
active_address: None,
persister, persister,
data_dir_path, data_dir_path,
status_stream,
}); });
sdk.status_stream.track_pending_swaps(sdk.clone())?;
Ok(sdk) Ok(sdk)
} }
@@ -132,13 +141,91 @@ impl LiquidSdk {
Ok(lsk.keypair) Ok(lsk.keypair)
} }
fn validate_state_transition(
from_state: PaymentState,
to_state: PaymentState,
) -> Result<(), PaymentError> {
match (from_state, to_state) {
(_, Created) => Err(PaymentError::Generic {
err: "Cannot transition to Created state".to_string(),
}),
(Created | Pending, Pending) => Ok(()),
(Complete | Failed, Pending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Pending state"),
}),
(Created | Pending, Complete) => Ok(()),
(Complete | Failed, Complete) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Complete state"),
}),
(_, Failed) => Ok(()),
}
}
/// Transitions a Receive swap to a new state
pub(crate) fn try_handle_receive_swap_update(
&self,
swap_id: &str,
to_state: PaymentState,
claim_tx_id: Option<&str>,
) -> Result<(), PaymentError> {
info!(
"Transitioning Receive swap {swap_id} to {to_state:?} (claim_tx_id = {claim_tx_id:?})"
);
let con = self.persister.get_connection()?;
let swap = Persister::fetch_swap_out(&con, swap_id)
.map_err(|_| PaymentError::PersistError)?
.ok_or(PaymentError::Generic {
err: format!("Swap Out not found {swap_id}"),
})?;
Self::validate_state_transition(swap.state, to_state)?;
self.persister
.try_handle_receive_swap_update(&con, swap_id, to_state, claim_tx_id)
}
/// Transitions a Send swap to a new state
pub(crate) fn try_handle_send_swap_update(
&self,
swap_id: &str,
to_state: PaymentState,
lockup_tx_id: Option<&str>,
refund_tx_id: Option<&str>,
) -> Result<(), PaymentError> {
info!("Transitioning Send swap {swap_id} to {to_state:?} (lockup_tx_id = {lockup_tx_id:?}, refund_tx_id = {refund_tx_id:?})");
let con = self.persister.get_connection()?;
let swap = Persister::fetch_swap_in(&con, swap_id)
.map_err(|_| PaymentError::PersistError)?
.ok_or(PaymentError::Generic {
err: format!("Swap In not found {swap_id}"),
})?;
Self::validate_state_transition(swap.state, to_state)?;
self.persister.try_handle_send_swap_update(
&con,
swap_id,
to_state,
lockup_tx_id,
refund_tx_id,
)
}
/// Handles status updates from Boltz for Receive swaps
pub(crate) fn try_handle_reverse_swap_status( pub(crate) fn try_handle_reverse_swap_status(
&self, &self,
swap_state: RevSwapStates, swap_state: RevSwapStates,
id: &str, id: &str,
) -> Result<()> { ) -> Result<()> {
self.sync()?;
info!("Handling reverse swap transition to {swap_state:?} for swap {id}");
let con = self.persister.get_connection()?; let con = self.persister.get_connection()?;
let ongoing_swap_out = Persister::fetch_ongoing_swap_out(&con, id)? let swap_out = Persister::fetch_swap_out(&con, id)?
.ok_or(anyhow!("No ongoing swap out found for ID {id}"))?; .ok_or(anyhow!("No ongoing swap out found for ID {id}"))?;
match swap_state { match swap_state {
@@ -146,78 +233,69 @@ impl LiquidSdk {
| RevSwapStates::InvoiceExpired | RevSwapStates::InvoiceExpired
| RevSwapStates::TransactionFailed | RevSwapStates::TransactionFailed
| RevSwapStates::TransactionRefunded => { | RevSwapStates::TransactionRefunded => {
warn!("Cannot claim swap {id}, unrecoverable state: {swap_state:?}"); error!("Swap {id} entered into an unrecoverable state: {swap_state:?}");
self.persister self.try_handle_receive_swap_update(id, Failed, None)?;
.resolve_ongoing_swap(id, None)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))?;
} }
// We may be offline, or claiming failued due to other reasons until the swap reached these states
// If an ongoing reverse swap is in any of these states, we should be able to claim // The lockup tx is in the mempool and we accept 0-conf => try to claim
// TODO Add 0-conf preconditions check: https://github.com/breez/breez-liquid-sdk/issues/187
RevSwapStates::TransactionMempool RevSwapStates::TransactionMempool
| RevSwapStates::TransactionConfirmed // The lockup tx is confirmed => try to claim
| RevSwapStates::InvoiceSettled => match self.try_claim_v2(&ongoing_swap_out) { | RevSwapStates::TransactionConfirmed => {
Ok(txid) => { match swap_out.claim_tx_id {
let payer_amount_sat = get_invoice_amount!(ongoing_swap_out.invoice); Some(claim_tx_id) => {
self.persister warn!("Claim tx for reverse swap {id} was already broadcast: txid {claim_tx_id}")
.resolve_ongoing_swap(
id,
Some((
txid,
PaymentData {
payer_amount_sat,
receiver_amount_sat: ongoing_swap_out.receiver_amount_sat,
},
)),
)
.map_err(|e| anyhow!("Could not resolve swap {id}: {e}"))?;
}
Err(err) => {
if let PaymentError::AlreadyClaimed = err {
warn!("Funds already claimed");
self.persister
.resolve_ongoing_swap(id, None)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))?;
} }
warn!("Could not claim swap {id} yet. Err: {err}"); None => match self.try_claim(&swap_out) {
Ok(()) => {}
Err(err) => match err {
PaymentError::AlreadyClaimed => warn!("Funds already claimed for reverse swap {id}"),
_ => error!("Claim reverse swap {id} failed: {err}")
}
},
} }
},
RevSwapStates::Created | RevSwapStates::MinerFeePaid => {
// Too soon to try to claim
} }
// Too soon to try to claim
RevSwapStates::Created | RevSwapStates::MinerFeePaid => {}
// Swap completed successfully (HODL invoice settled), the claim already happened
RevSwapStates::InvoiceSettled => {}
} }
Ok(()) Ok(())
} }
/// Handles status updates from Boltz for Send swaps
pub(crate) fn try_handle_submarine_swap_status( pub(crate) fn try_handle_submarine_swap_status(
&self, &self,
swap_state: SubSwapStates, swap_state: SubSwapStates,
id: &str, id: &str,
) -> Result<()> { ) -> Result<()> {
let con = self.persister.get_connection()?; self.sync()?;
let ongoing_swap_in = Persister::fetch_ongoing_swap_in(&con, id)?
.ok_or(anyhow!("No ongoing swap in found for ID {id}"))?; info!("Handling submarine swap transition to {swap_state:?} for swap {id}");
let con = self.persister.get_connection()?;
let ongoing_swap_in = Persister::fetch_swap_in(&con, id)?
.ok_or(anyhow!("No ongoing swap in found for ID {id}"))?;
let create_response: CreateSubmarineResponse =
ongoing_swap_in.get_boltz_create_response()?;
let Some(txid) = ongoing_swap_in.lockup_txid.clone() else {
return Err(anyhow!("Swap-in {id} is pending but no txid is present"));
};
let receiver_amount_sat = get_invoice_amount!(ongoing_swap_in.invoice); let receiver_amount_sat = get_invoice_amount!(ongoing_swap_in.invoice);
let keypair = self.get_submarine_keys(0)?; let keypair = self.get_submarine_keys(0)?;
let create_response: CreateSubmarineResponse =
serde_json::from_str(&ongoing_swap_in.create_response_json)?;
match swap_state { match swap_state {
SubSwapStates::TransactionClaimPending => { SubSwapStates::TransactionClaimPending => {
let Ok(swap_script) = LBtcSwapScriptV2::submarine_from_swap_resp( let lockup_tx_id = ongoing_swap_in.lockup_tx_id.ok_or(anyhow!(
"Swap-in {id} is pending but no lockup txid is present"
))?;
let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp(
&create_response, &create_response,
keypair.public_key().into(), keypair.public_key().into(),
) else { )
self.persister .map_err(|e| anyhow!("Could not rebuild refund details for swap-in {id}: {e:?}"))?;
.resolve_ongoing_swap(id, None)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))?;
return Err(anyhow!("Could not rebuild refund details for swap-in {id}"));
};
self.post_submarine_claim_details( self.post_submarine_claim_details(
id, id,
@@ -227,98 +305,89 @@ impl LiquidSdk {
) )
.map_err(|e| anyhow!("Could not post claim details. Err: {e:?}"))?; .map_err(|e| anyhow!("Could not post claim details. Err: {e:?}"))?;
self.persister // We insert a pseudo-lockup-tx in case LWK fails to pick up the new mempool tx for a while
.resolve_ongoing_swap( // This makes the tx known to the SDK (get_info, list_payments) instantly
id, self.persister.insert_or_update_payment(PaymentTxData {
Some(( tx_id: lockup_tx_id,
txid, timestamp: None,
PaymentData { amount_sat: ongoing_swap_in.payer_amount_sat,
payer_amount_sat: ongoing_swap_in.payer_amount_sat, payment_type: PaymentType::Send,
receiver_amount_sat, is_confirmed: false,
}, })?;
)),
)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))?;
Ok(()) Ok(())
} }
SubSwapStates::TransactionClaimed => { SubSwapStates::TransactionClaimed => {
warn!("Swap-in {id} has already been claimed. Resolving..."); warn!("Swap-in {id} has already been claimed");
// TODO Verify preimage, or check that lockup funds are spent
self.persister
.resolve_ongoing_swap(
id,
Some((
txid,
PaymentData {
payer_amount_sat: ongoing_swap_in.payer_amount_sat,
receiver_amount_sat,
},
)),
)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))?;
warn!("Swap-in {id} resolved successfully");
Ok(()) Ok(())
} }
SubSwapStates::TransactionLockupFailed SubSwapStates::TransactionLockupFailed
| SubSwapStates::InvoiceFailedToPay | SubSwapStates::InvoiceFailedToPay
| SubSwapStates::SwapExpired => { | SubSwapStates::SwapExpired => {
warn!("Swap-in {id} is in an unrecoverable state: {swap_state:?}"); warn!("Swap-in {id} is in an unrecoverable state: {swap_state:?}");
// If swap state is unrecoverable, try refunding // If swap state is unrecoverable, try refunding
let Ok(swap_script) = LBtcSwapScriptV2::submarine_from_swap_resp( let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp(
&create_response, &create_response,
keypair.public_key().into(), keypair.public_key().into(),
) else { )
self.persister .map_err(|e| anyhow!("Could not rebuild refund details for swap-in {id}: {e:?}"))?;
.resolve_ongoing_swap(id, None)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))?;
return Err(anyhow!("Could not rebuild refund details for swap-in {id}")); let refund_tx_id =
};
let refund_txid =
self.try_refund(id, &swap_script, &keypair, receiver_amount_sat)?; self.try_refund(id, &swap_script, &keypair, receiver_amount_sat)?;
info!("Broadcast refund tx for Swap-in {id}. Tx id: {refund_tx_id}");
self.try_handle_send_swap_update(id, Pending, None, Some(&refund_tx_id))?;
warn!("Swap-in {id} refunded successfully. Txid: {refund_txid}"); Ok(())
self.persister
.resolve_ongoing_swap(id, None)
.map_err(|_| anyhow!("Could not resolve swap {id} in database"))
} }
_ => Err(anyhow!("New state for submarine swap {id}: {swap_state:?}")), _ => Err(anyhow!("New state for submarine swap {id}: {swap_state:?}")),
} }
} }
pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<OngoingSwap>> { pub(crate) fn list_ongoing_swaps(&self) -> Result<Vec<Swap>> {
self.persister.list_ongoing_swaps() self.persister.list_ongoing_swaps()
} }
fn scan(&self) -> Result<(), lwk_wollet::Error> { /// Gets the next unused onchain Liquid address
let mut electrum_client = ElectrumClient::new(&self.electrum_url)?; fn next_unused_address(&self) -> Result<Address, lwk_wollet::Error> {
let mut lwk_wollet = self.lwk_wollet.lock().unwrap();
full_scan_with_electrum_client(&mut lwk_wollet, &mut electrum_client)
}
fn address(&self) -> Result<Address, lwk_wollet::Error> {
let lwk_wollet = self.lwk_wollet.lock().unwrap(); let lwk_wollet = self.lwk_wollet.lock().unwrap();
Ok(lwk_wollet.address(self.active_address)?.address().clone()) Ok(lwk_wollet.address(None)?.address().clone())
}
fn total_balance_sat(&self, with_scan: bool) -> Result<u64> {
if with_scan {
self.scan()?;
}
let balance = self.lwk_wollet.lock().unwrap().balance()?;
Ok(balance.values().sum())
} }
pub fn get_info(&self, req: GetInfoRequest) -> Result<GetInfoResponse> { pub fn get_info(&self, req: GetInfoRequest) -> Result<GetInfoResponse> {
debug!("active_address: {}", self.address()?); debug!("next_unused_address: {}", self.next_unused_address()?);
if req.with_scan {
self.sync()?;
}
let mut pending_send_sat = 0;
let mut pending_receive_sat = 0;
let mut confirmed_sent_sat = 0;
let mut confirmed_received_sat = 0;
for p in self.list_payments()? {
match p.payment_type {
PaymentType::Send => match p.status {
PaymentState::Complete => confirmed_sent_sat += p.amount_sat,
PaymentState::Failed => {}
_ => pending_send_sat += p.amount_sat,
},
PaymentType::Receive => match p.status {
PaymentState::Complete => confirmed_received_sat += p.amount_sat,
PaymentState::Failed => {}
_ => pending_receive_sat += p.amount_sat,
},
}
}
Ok(GetInfoResponse { Ok(GetInfoResponse {
balance_sat: self.total_balance_sat(req.with_scan)?, balance_sat: confirmed_received_sat - confirmed_sent_sat - pending_send_sat,
pending_send_sat,
pending_receive_sat,
pubkey: self.lwk_signer.xpub().public_key.to_string(), pubkey: self.lwk_signer.xpub().public_key.to_string(),
}) })
} }
@@ -350,9 +419,20 @@ impl LiquidSdk {
recipient_address: &str, recipient_address: &str,
amount_sat: u64, amount_sat: u64,
) -> Result<Transaction, PaymentError> { ) -> Result<Transaction, PaymentError> {
self.scan()?;
let lwk_wollet = self.lwk_wollet.lock().unwrap(); let lwk_wollet = self.lwk_wollet.lock().unwrap();
let mut pset = lwk_wollet.send_lbtc(amount_sat, recipient_address, fee_rate)?; let mut pset = lwk_wollet::TxBuilder::new(self.network.into())
.add_lbtc_recipient(
&ElementsAddress::from_str(recipient_address).map_err(|e| {
PaymentError::Generic {
err: format!(
"Recipient address {recipient_address} is not a valid ElementsAddress: {e:?}"
),
}
})?,
amount_sat,
)?
.fee_rate(fee_rate)
.finish(&lwk_wollet)?;
let signer = AnySigner::Software(self.lwk_signer.clone()); let signer = AnySigner::Software(self.lwk_signer.clone());
signer.sign(&mut pset)?; signer.sign(&mut pset)?;
Ok(lwk_wollet.finalize(&mut pset)?) Ok(lwk_wollet.finalize(&mut pset)?)
@@ -475,17 +555,17 @@ impl LiquidSdk {
) { ) {
// Try with cooperative refund // Try with cooperative refund
Ok(tx) => { Ok(tx) => {
let txid = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?;
debug!("Successfully broadcast cooperative refund for swap-in {swap_id}"); debug!("Successfully broadcast cooperative refund for swap-in {swap_id}");
Ok(txid) Ok(refund_tx_id)
} }
// Try with non-cooperative refund // Try with non-cooperative refund
Err(e) => { Err(e) => {
debug!("Cooperative refund failed: {:?}", e); debug!("Cooperative refund failed: {:?}", e);
let tx = refund_tx.sign_refund(keypair, broadcast_fees_sat, None)?; let tx = refund_tx.sign_refund(keypair, broadcast_fees_sat, None)?;
let txid = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?;
debug!("Successfully broadcast non-cooperative refund for swap-in {swap_id}"); debug!("Successfully broadcast non-cooperative refund for swap-in {swap_id}");
Ok(txid) Ok(refund_tx_id)
} }
} }
} }
@@ -506,6 +586,8 @@ impl LiquidSdk {
debug!("Received claim tx details: {:?}", &claim_tx_response); debug!("Received claim tx details: {:?}", &claim_tx_response);
Self::verify_payment_hash(&claim_tx_response.preimage, invoice)?; Self::verify_payment_hash(&claim_tx_response.preimage, invoice)?;
// After we confirm the preimage is correct, we mark this as complete
self.try_handle_send_swap_update(swap_id, Complete, None, None)?;
let (partial_sig, pub_nonce) = let (partial_sig, pub_nonce) =
refund_tx.submarine_partial_sig(keypair, &claim_tx_response)?; refund_tx.submarine_partial_sig(keypair, &claim_tx_response)?;
@@ -532,29 +614,24 @@ impl LiquidSdk {
)?; )?;
let electrum_client = ElectrumClient::new(&self.electrum_url)?; let electrum_client = ElectrumClient::new(&self.electrum_url)?;
let lockup_txid = electrum_client.broadcast(&lockup_tx)?.to_string(); let lockup_tx_id = electrum_client.broadcast(&lockup_tx)?.to_string();
debug!( debug!(
"Successfully broadcast lockup transaction for swap-in {swap_id}. Txid: {lockup_txid}" "Successfully broadcast lockup transaction for swap-in {swap_id}. Lockup tx id: {lockup_tx_id}"
); );
Ok(lockup_txid) Ok(lockup_tx_id)
} }
pub fn send_payment( pub fn send_payment(
&self, &self,
req: &PrepareSendResponse, req: &PrepareSendResponse,
) -> Result<SendPaymentResponse, PaymentError> { ) -> Result<SendPaymentResponse, PaymentError> {
let invoice = self.validate_invoice(&req.invoice)?; self.validate_invoice(&req.invoice)?;
let receiver_amount_sat = invoice let receiver_amount_sat = get_invoice_amount!(req.invoice);
.amount_milli_satoshis()
.ok_or(PaymentError::AmountOutOfRange)?
/ 1000;
let client = self.boltz_client_v2(); let client = self.boltz_client_v2();
let lbtc_pair = Self::validate_submarine_pairs(&client, receiver_amount_sat)?; let lbtc_pair = Self::validate_submarine_pairs(&client, receiver_amount_sat)?;
let broadcast_fees_sat = self.get_broadcast_fee_estimation(receiver_amount_sat)?; let broadcast_fees_sat = self.get_broadcast_fee_estimation(receiver_amount_sat)?;
ensure_sdk!( ensure_sdk!(
req.fees_sat == lbtc_pair.fees.total(receiver_amount_sat) + broadcast_fees_sat, req.fees_sat == lbtc_pair.fees.total(receiver_amount_sat) + broadcast_fees_sat,
PaymentError::InvalidOrExpiredFees PaymentError::InvalidOrExpiredFees
@@ -572,23 +649,20 @@ impl LiquidSdk {
invoice: req.invoice.to_string(), invoice: req.invoice.to_string(),
refund_public_key, refund_public_key,
pair_hash: Some(lbtc_pair.hash), pair_hash: Some(lbtc_pair.hash),
// TODO: Add referral id
referral_id: None, referral_id: None,
})?; })?;
let create_response_json =
serde_json::to_string(&create_response).map_err(|_| PaymentError::Generic {
err: "Could not store swap response locally".to_string(),
})?;
let swap_id = &create_response.id; let swap_id = &create_response.id;
let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp( let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp(
&create_response, &create_response,
keypair.public_key().into(), keypair.public_key().into(),
)?; )?;
let create_response_json = SwapIn::from_boltz_struct_to_json(&create_response, swap_id)?;
debug!("Opening WS connection for swap {}", &swap_id); debug!("Opening WS connection for swap {swap_id}");
let mut socket = client.connect_ws()?; let mut socket = client.connect_ws()?;
set_stream_nonblocking(socket.get_mut())?;
let subscription = Subscription::new(swap_id); let subscription = Subscription::new(swap_id);
let subscribe_json = serde_json::to_string(&subscription) let subscribe_json = serde_json::to_string(&subscription)
.map_err(|e| anyhow!("Failed to serialize subscription msg: {e:?}"))?; .map_err(|e| anyhow!("Failed to serialize subscription msg: {e:?}"))?;
@@ -596,21 +670,23 @@ impl LiquidSdk {
.send(tungstenite::Message::Text(subscribe_json)) .send(tungstenite::Message::Text(subscribe_json))
.map_err(|e| anyhow!("Failed to subscribe to websocket updates: {e:?}"))?; .map_err(|e| anyhow!("Failed to subscribe to websocket updates: {e:?}"))?;
// We insert the pending send to avoid it being handled by the status stream // We mark the pending send as already tracked to avoid it being handled by the status stream
self.status_stream BoltzStatusStream::mark_swap_as_tracked(swap_id, SwapType::Submarine);
.insert_tracked_swap(swap_id, SwapType::Submarine);
self.persister self.persister.insert_swap_in(SwapIn {
.insert_or_update_ongoing_swap_in(OngoingSwapIn { id: swap_id.clone(),
id: swap_id.clone(), invoice: req.invoice.clone(),
invoice: req.invoice.clone(), payer_amount_sat: req.fees_sat + receiver_amount_sat,
payer_amount_sat: req.fees_sat + receiver_amount_sat, receiver_amount_sat,
create_response_json: create_response_json.clone(), create_response_json,
lockup_txid: None, lockup_tx_id: None,
})?; refund_tx_id: None,
created_at: utils::now(),
state: PaymentState::Created,
})?;
let result; let result;
let mut lockup_txid = String::new(); let mut lockup_tx_id = String::new();
loop { loop {
let data = match utils::get_swap_status_v2(&mut socket, swap_id) { let data = match utils::get_swap_status_v2(&mut socket, swap_id) {
Ok(data) => data, Ok(data) => data,
@@ -619,13 +695,15 @@ impl LiquidSdk {
continue; continue;
} }
}; };
let state = data let state = data
.parse::<SubSwapStates>() .parse::<SubSwapStates>()
.map_err(|_| PaymentError::Generic { .map_err(|_| PaymentError::Generic {
err: "Invalid state received from swapper".to_string(), err: "Invalid state received from swapper".to_string(),
})?; })?;
// Sync before handling new state
self.sync()?;
// See https://docs.boltz.exchange/v/api/lifecycle#normal-submarine-swaps // See https://docs.boltz.exchange/v/api/lifecycle#normal-submarine-swaps
match state { match state {
// Boltz has locked the HTLC, we proceed with locking up the funds // Boltz has locked the HTLC, we proceed with locking up the funds
@@ -633,52 +711,34 @@ impl LiquidSdk {
// Check that we have not persisted the swap already // Check that we have not persisted the swap already
let con = self.persister.get_connection()?; let con = self.persister.get_connection()?;
if let Some(ongoing_swap) = Persister::fetch_ongoing_swap_in(&con, swap_id) if let Some(ongoing_swap) = Persister::fetch_swap_in(&con, swap_id)
.map_err(|_| PaymentError::PersistError)? .map_err(|_| PaymentError::PersistError)?
{ {
if ongoing_swap.lockup_txid.is_some() { if ongoing_swap.lockup_tx_id.is_some() {
continue; continue;
} }
}; };
lockup_txid = self.lockup_funds(swap_id, &create_response)?; lockup_tx_id = self.lockup_funds(swap_id, &create_response)?;
self.persister self.try_handle_send_swap_update(swap_id, Pending, Some(&lockup_tx_id), None)?;
.insert_or_update_ongoing_swap_in(OngoingSwapIn {
id: swap_id.clone(),
invoice: req.invoice.clone(),
payer_amount_sat: req.fees_sat + receiver_amount_sat,
create_response_json: create_response_json.clone(),
lockup_txid: Some(lockup_txid.clone()),
})?;
} }
// Boltz has detected the lockup in the mempool, we can speed up // Boltz has detected the lockup in the mempool, we can speed up
// the claim by doing so cooperatively // the claim by doing so cooperatively
SubSwapStates::TransactionClaimPending => { SubSwapStates::TransactionClaimPending => {
// TODO Consolidate status handling: merge with and reuse try_handle_submarine_swap_status
self.post_submarine_claim_details( self.post_submarine_claim_details(
swap_id, swap_id,
&swap_script, &swap_script,
&req.invoice, &req.invoice,
&keypair, &keypair,
)?; )?;
debug!("Boltz successfully claimed the funds");
debug!("Boltz successfully claimed the funds. Resolving swap-in {swap_id}"); BoltzStatusStream::unmark_swap_as_tracked(swap_id, SwapType::ReverseSubmarine);
self.persister.resolve_ongoing_swap(
swap_id,
Some((
lockup_txid.clone(),
PaymentData {
payer_amount_sat: receiver_amount_sat + req.fees_sat,
receiver_amount_sat,
},
)),
)?;
self.status_stream result = Ok(SendPaymentResponse { txid: lockup_tx_id });
.resolve_tracked_swap(swap_id, SwapType::ReverseSubmarine);
// TODO: Change lockup txid to claim txid
result = Ok(SendPaymentResponse { txid: lockup_txid });
debug!("Successfully resolved swap-in {swap_id}"); debug!("Successfully resolved swap-in {swap_id}");
break; break;
@@ -692,7 +752,7 @@ impl LiquidSdk {
SubSwapStates::InvoiceFailedToPay SubSwapStates::InvoiceFailedToPay
| SubSwapStates::SwapExpired | SubSwapStates::SwapExpired
| SubSwapStates::TransactionLockupFailed => { | SubSwapStates::TransactionLockupFailed => {
let refund_txid = let refund_tx_id =
self.try_refund(swap_id, &swap_script, &keypair, receiver_amount_sat)?; self.try_refund(swap_id, &swap_script, &keypair, receiver_amount_sat)?;
result = Err(PaymentError::Refunded { result = Err(PaymentError::Refunded {
@@ -700,35 +760,38 @@ impl LiquidSdk {
"Unrecoverable state for swap-in {swap_id}: {}", "Unrecoverable state for swap-in {swap_id}: {}",
state.to_string() state.to_string()
), ),
txid: refund_txid.clone(), refund_tx_id,
}); });
break; break;
} }
_ => {} _ => {}
}; };
sleep(Duration::from_millis(500));
} }
socket.close(None).unwrap(); socket.close(None).unwrap();
result result
} }
fn try_claim_v2(&self, ongoing_swap_out: &OngoingSwapOut) -> Result<String, PaymentError> { fn try_claim(&self, ongoing_swap_out: &SwapOut) -> Result<(), PaymentError> {
debug!("Trying to claim reverse swap {}", &ongoing_swap_out.id); ensure_sdk!(
ongoing_swap_out.claim_tx_id.is_none(),
PaymentError::AlreadyClaimed
);
let rev_swap_id = &ongoing_swap_out.id;
debug!("Trying to claim reverse swap {rev_swap_id}",);
let lsk = self.get_liquid_swap_key()?; let lsk = self.get_liquid_swap_key()?;
let our_keys = lsk.keypair; let our_keys = lsk.keypair;
let create_response: CreateReverseResponse = let create_response = ongoing_swap_out.get_boltz_create_response()?;
serde_json::from_str(&ongoing_swap_out.redeem_script).unwrap();
let swap_script = LBtcSwapScriptV2::reverse_from_swap_resp( let swap_script = LBtcSwapScriptV2::reverse_from_swap_resp(
&create_response, &create_response,
our_keys.public_key().into(), our_keys.public_key().into(),
)?; )?;
let claim_address = self.address()?.to_string(); let claim_address = self.next_unused_address()?.to_string();
let claim_tx = LBtcSwapTxV2::new_claim( let claim_tx_wrapper = LBtcSwapTxV2::new_claim(
swap_script, swap_script,
claim_address, claim_address,
&self.network_config(), &self.network_config(),
@@ -736,54 +799,36 @@ impl LiquidSdk {
ongoing_swap_out.id.clone(), ongoing_swap_out.id.clone(),
)?; )?;
let tx = claim_tx.sign_claim( let claim_tx = claim_tx_wrapper.sign_claim(
&our_keys, &our_keys,
&Preimage::from_str(&ongoing_swap_out.preimage)?, &Preimage::from_str(&ongoing_swap_out.preimage)?,
Amount::from_sat(ongoing_swap_out.claim_fees_sat), Amount::from_sat(ongoing_swap_out.claim_fees_sat),
// Enable cooperative claim (Some) or not (None) // Enable cooperative claim (Some) or not (None)
Some((&self.boltz_client_v2(), ongoing_swap_out.id.clone())), Some((&self.boltz_client_v2(), rev_swap_id.clone())),
// None // None
)?; )?;
// Electrum only broadcasts txs with lowball fees on testnet let claim_tx_id = claim_tx_wrapper.broadcast(
// For mainnet, we use Boltz to broadcast &claim_tx,
match self.network { &self.network_config(),
Network::Liquid => { Some((&self.boltz_client_v2(), self.network.into())),
let tx_hex = elements::encode::serialize(&tx).to_lower_hex_string(); )?;
let response = self info!("Successfully broadcast claim tx {claim_tx_id} for rev swap {rev_swap_id}");
.boltz_client_v2() debug!("Claim Tx {:?}", claim_tx);
.broadcast_tx(self.network.into(), &tx_hex)?;
info!("Claim broadcast response: {response:?}");
}
Network::LiquidTestnet => {
let electrum_client = ElectrumClient::new(&self.electrum_url)?;
electrum_client.broadcast(&tx)?;
}
};
info!("Succesfully broadcasted claim tx {}", tx.txid()); self.try_handle_receive_swap_update(rev_swap_id, Pending, Some(&claim_tx_id))?;
debug!("Claim Tx {:?}", tx);
Ok(tx.txid().to_string()) // We insert a pseudo-claim-tx in case LWK fails to pick up the new mempool tx for a while
} // This makes the tx known to the SDK (get_info, list_payments) instantly
self.persister.insert_or_update_payment(PaymentTxData {
tx_id: claim_tx_id,
timestamp: None,
amount_sat: ongoing_swap_out.receiver_amount_sat,
payment_type: PaymentType::Receive,
is_confirmed: false,
})?;
#[allow(dead_code)] Ok(())
fn validate_reverse_pairs(
client: &BoltzApiClientV2,
payer_amount_sat: u64,
) -> Result<ReversePair, PaymentError> {
let lbtc_pair = client
.get_reverse_pairs()?
.get_btc_to_lbtc_pair()
.ok_or(PaymentError::PairsNotFound)?;
lbtc_pair.limits.within(payer_amount_sat)?;
let fees_sat = lbtc_pair.fees.total(payer_amount_sat);
ensure_sdk!(payer_amount_sat > fees_sat, PaymentError::AmountOutOfRange);
Ok(lbtc_pair)
} }
pub fn prepare_receive_payment( pub fn prepare_receive_payment(
@@ -849,18 +894,9 @@ impl LiquidSdk {
}; };
let create_response = self.boltz_client_v2().post_reverse_req(v2_req)?; let create_response = self.boltz_client_v2().post_reverse_req(v2_req)?;
// TODO Persisting this in the DB (reusing "redeem_script" field), as we need it later when claiming let swap_id = create_response.id.clone();
let redeem_script = serde_json::to_string(&create_response).unwrap();
let swap_id = create_response.id;
let invoice = Bolt11Invoice::from_str(&create_response.invoice) let invoice = Bolt11Invoice::from_str(&create_response.invoice)
.map_err(|_| PaymentError::InvalidInvoice)?; .map_err(|_| PaymentError::InvalidInvoice)?;
let blinding_str =
create_response
.blinding_key
.ok_or(boltz_client::error::Error::Protocol(
"Boltz response does not contain a blinding key.".to_string(),
))?;
let payer_amount_sat = invoice let payer_amount_sat = invoice
.amount_milli_satoshis() .amount_milli_satoshis()
.ok_or(PaymentError::InvalidInvoice)? .ok_or(PaymentError::InvalidInvoice)?
@@ -872,15 +908,20 @@ impl LiquidSdk {
return Err(PaymentError::InvalidInvoice); return Err(PaymentError::InvalidInvoice);
}; };
let create_response_json =
SwapOut::from_boltz_struct_to_json(&create_response, &swap_id, &invoice.to_string())?;
self.persister self.persister
.insert_or_update_ongoing_swap_out(OngoingSwapOut { .insert_swap_out(SwapOut {
id: swap_id.clone(), id: swap_id.clone(),
preimage: preimage_str, preimage: preimage_str,
blinding_key: blinding_str, create_response_json,
redeem_script,
invoice: invoice.to_string(), invoice: invoice.to_string(),
payer_amount_sat,
receiver_amount_sat: payer_amount_sat - req.fees_sat, receiver_amount_sat: payer_amount_sat - req.fees_sat,
claim_fees_sat: reverse_pair.fees.claim_estimate(), claim_fees_sat: reverse_pair.fees.claim_estimate(),
claim_tx_id: None,
created_at: utils::now(),
state: PaymentState::Created,
}) })
.map_err(|_| PaymentError::PersistError)?; .map_err(|_| PaymentError::PersistError)?;
@@ -890,42 +931,57 @@ impl LiquidSdk {
}) })
} }
pub fn list_payments(&self, with_scan: bool, include_pending: bool) -> Result<Vec<Payment>> { /// This method fetches the chain tx data (onchain and mempool) using LWK. For every wallet tx,
/// it inserts or updates a corresponding entry in our Payments table.
fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> {
if with_scan { if with_scan {
self.scan()?; let mut electrum_client = ElectrumClient::new(&self.electrum_url)?;
let mut lwk_wollet = self.lwk_wollet.lock().unwrap();
lwk_wollet::full_scan_with_electrum_client(&mut lwk_wollet, &mut electrum_client)?;
} }
let transactions = self.lwk_wollet.lock().unwrap().transactions()?; let con = self.persister.get_connection()?;
let pending_receive_swaps_by_claim_tx_id: HashMap<String, SwapOut> = self
.persister
.list_pending_receive_swaps_by_claim_tx_id(&con)?;
let pending_send_swaps_by_refund_tx_id: HashMap<String, SwapIn> = self
.persister
.list_pending_send_swaps_by_refund_tx_id(&con)?;
let payment_data = self.persister.get_payment_data()?; for tx in self.lwk_wollet.lock().unwrap().transactions()? {
let mut payments: Vec<Payment> = transactions let tx_id = tx.txid.to_string();
.iter() let is_tx_confirmed = tx.height.is_some();
.map(|tx| { let amount_sat = tx.balance.values().sum::<i64>();
let id = tx.txid.to_string();
let data = payment_data.get(&id);
let amount_sat = tx.balance.values().sum::<i64>();
let fees_sat = data.map(|d| d.payer_amount_sat - d.receiver_amount_sat);
Payment { // Transition the swaps whose state depends on this tx being confirmed
id: Some(id.clone()), if is_tx_confirmed {
timestamp: tx.timestamp, if let Some(swap) = pending_receive_swaps_by_claim_tx_id.get(&tx_id) {
amount_sat: amount_sat.unsigned_abs(), self.try_handle_receive_swap_update(&swap.id, Complete, None)?;
payment_type: match amount_sat >= 0 { }
true => PaymentType::Received, if let Some(swap) = pending_send_swaps_by_refund_tx_id.get(&tx_id) {
false => PaymentType::Sent, self.try_handle_send_swap_update(&swap.id, Failed, None, None)?;
},
invoice: None,
fees_sat,
} }
})
.collect();
if include_pending {
for swap in self.persister.list_ongoing_swaps()? {
payments.insert(0, swap.into());
} }
self.persister.insert_or_update_payment(PaymentTxData {
tx_id,
timestamp: tx.timestamp,
amount_sat: amount_sat.unsigned_abs(),
payment_type: match amount_sat >= 0 {
true => PaymentType::Receive,
false => PaymentType::Send,
},
is_confirmed: is_tx_confirmed,
})?;
} }
Ok(())
}
/// Lists the SDK payments. The payments are determined based on onchain transactions and swaps.
pub fn list_payments(&self) -> Result<Vec<Payment>> {
let mut payments: Vec<Payment> = self.persister.get_payments()?.values().cloned().collect();
payments.sort_by_key(|p| p.timestamp);
Ok(payments) Ok(payments)
} }
@@ -949,6 +1005,16 @@ impl LiquidSdk {
self.persister.restore_from_backup(backup_path) self.persister.restore_from_backup(backup_path)
} }
/// Synchronize the DB with mempool and onchain data
pub fn sync(&self) -> Result<()> {
let t0 = Instant::now();
self.sync_payments_with_chain_data(true)?;
let duration_ms = Instant::now().duration_since(t0).as_millis();
info!("Synchronized with mempool and onchain data (t = {duration_ms} ms)");
Ok(())
}
pub fn backup(&self) -> Result<()> { pub fn backup(&self) -> Result<()> {
self.persister.backup() self.persister.backup()
} }
@@ -990,13 +1056,11 @@ mod tests {
} }
fn list_pending(sdk: &LiquidSdk) -> Result<Vec<Payment>> { fn list_pending(sdk: &LiquidSdk) -> Result<Vec<Payment>> {
let payments = sdk.list_payments(true, true)?; let payments = sdk.list_payments()?;
Ok(payments Ok(payments
.iter() .iter()
.filter(|p| { .filter(|p| matches!(&p.status, PaymentStatus::Pending))
[PaymentType::PendingSend, PaymentType::PendingReceive].contains(&p.payment_type)
})
.cloned() .cloned()
.collect()) .collect())
} }

View File

@@ -1,10 +1,14 @@
use std::net::TcpStream; use std::net::TcpStream;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, ensure, Result}; use anyhow::{anyhow, ensure, Result};
use boltz_client::swaps::boltzv2::SwapUpdate; use boltz_client::swaps::boltzv2::SwapUpdate;
use log::{error, info}; use log::{error, info};
use tungstenite::{stream::MaybeTlsStream, WebSocket}; use tungstenite::{stream::MaybeTlsStream, WebSocket};
use crate::error::PaymentError;
/// Fetch the swap status using the websocket endpoint /// Fetch the swap status using the websocket endpoint
pub(crate) fn get_swap_status_v2( pub(crate) fn get_swap_status_v2(
socket: &mut WebSocket<MaybeTlsStream<TcpStream>>, socket: &mut WebSocket<MaybeTlsStream<TcpStream>>,
@@ -65,3 +69,16 @@ pub(crate) fn get_swap_status_v2(
} }
} }
} }
pub(crate) fn now() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u32
}
pub(crate) fn json_to_pubkey(json: &str) -> Result<boltz_client::PublicKey, PaymentError> {
boltz_client::PublicKey::from_str(json).map_err(|e| PaymentError::Generic {
err: format!("Failed to deserialize PublicKey: {e:?}"),
})
}

View File

@@ -26,8 +26,7 @@ Future<PrepareReceiveResponse> prepareReceivePayment({required PrepareReceiveReq
Future<ReceivePaymentResponse> receivePayment({required PrepareReceiveResponse req, dynamic hint}) => Future<ReceivePaymentResponse> receivePayment({required PrepareReceiveResponse req, dynamic hint}) =>
RustLib.instance.api.receivePayment(req: req, hint: hint); RustLib.instance.api.receivePayment(req: req, hint: hint);
Future<List<Payment>> listPayments({required bool withScan, required bool includePending, dynamic hint}) => Future<List<Payment>> listPayments({dynamic hint}) => RustLib.instance.api.listPayments(hint: hint);
RustLib.instance.api.listPayments(withScan: withScan, includePending: includePending, hint: hint);
Future<void> emptyWalletCache({dynamic hint}) => RustLib.instance.api.emptyWalletCache(hint: hint); Future<void> emptyWalletCache({dynamic hint}) => RustLib.instance.api.emptyWalletCache(hint: hint);

View File

@@ -28,7 +28,7 @@ sealed class PaymentError with _$PaymentError implements FrbException {
const factory PaymentError.persistError() = PaymentError_PersistError; const factory PaymentError.persistError() = PaymentError_PersistError;
const factory PaymentError.refunded({ const factory PaymentError.refunded({
required String err, required String err,
required String txid, required String refundTxId,
}) = PaymentError_Refunded; }) = PaymentError_Refunded;
const factory PaymentError.sendError({ const factory PaymentError.sendError({
required String err, required String err,

View File

@@ -511,7 +511,7 @@ abstract class _$$PaymentError_RefundedImplCopyWith<$Res> {
_$PaymentError_RefundedImpl value, $Res Function(_$PaymentError_RefundedImpl) then) = _$PaymentError_RefundedImpl value, $Res Function(_$PaymentError_RefundedImpl) then) =
__$$PaymentError_RefundedImplCopyWithImpl<$Res>; __$$PaymentError_RefundedImplCopyWithImpl<$Res>;
@useResult @useResult
$Res call({String err, String txid}); $Res call({String err, String refundTxId});
} }
/// @nodoc /// @nodoc
@@ -526,16 +526,16 @@ class __$$PaymentError_RefundedImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? err = null, Object? err = null,
Object? txid = null, Object? refundTxId = null,
}) { }) {
return _then(_$PaymentError_RefundedImpl( return _then(_$PaymentError_RefundedImpl(
err: null == err err: null == err
? _value.err ? _value.err
: err // ignore: cast_nullable_to_non_nullable : err // ignore: cast_nullable_to_non_nullable
as String, as String,
txid: null == txid refundTxId: null == refundTxId
? _value.txid ? _value.refundTxId
: txid // ignore: cast_nullable_to_non_nullable : refundTxId // ignore: cast_nullable_to_non_nullable
as String, as String,
)); ));
} }
@@ -544,16 +544,16 @@ class __$$PaymentError_RefundedImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$PaymentError_RefundedImpl extends PaymentError_Refunded { class _$PaymentError_RefundedImpl extends PaymentError_Refunded {
const _$PaymentError_RefundedImpl({required this.err, required this.txid}) : super._(); const _$PaymentError_RefundedImpl({required this.err, required this.refundTxId}) : super._();
@override @override
final String err; final String err;
@override @override
final String txid; final String refundTxId;
@override @override
String toString() { String toString() {
return 'PaymentError.refunded(err: $err, txid: $txid)'; return 'PaymentError.refunded(err: $err, refundTxId: $refundTxId)';
} }
@override @override
@@ -562,11 +562,11 @@ class _$PaymentError_RefundedImpl extends PaymentError_Refunded {
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$PaymentError_RefundedImpl && other is _$PaymentError_RefundedImpl &&
(identical(other.err, err) || other.err == err) && (identical(other.err, err) || other.err == err) &&
(identical(other.txid, txid) || other.txid == txid)); (identical(other.refundTxId, refundTxId) || other.refundTxId == refundTxId));
} }
@override @override
int get hashCode => Object.hash(runtimeType, err, txid); int get hashCode => Object.hash(runtimeType, err, refundTxId);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@@ -576,12 +576,12 @@ class _$PaymentError_RefundedImpl extends PaymentError_Refunded {
} }
abstract class PaymentError_Refunded extends PaymentError { abstract class PaymentError_Refunded extends PaymentError {
const factory PaymentError_Refunded({required final String err, required final String txid}) = const factory PaymentError_Refunded({required final String err, required final String refundTxId}) =
_$PaymentError_RefundedImpl; _$PaymentError_RefundedImpl;
const PaymentError_Refunded._() : super._(); const PaymentError_Refunded._() : super._();
String get err; String get err;
String get txid; String get refundTxId;
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$PaymentError_RefundedImplCopyWith<_$PaymentError_RefundedImpl> get copyWith => _$$PaymentError_RefundedImplCopyWith<_$PaymentError_RefundedImpl> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;

View File

@@ -71,7 +71,7 @@ abstract class RustLibApi extends BaseApi {
Future<GetInfoResponse> getInfo({required GetInfoRequest req, dynamic hint}); Future<GetInfoResponse> getInfo({required GetInfoRequest req, dynamic hint});
Future<List<Payment>> listPayments({required bool withScan, required bool includePending, dynamic hint}); Future<List<Payment>> listPayments({dynamic hint});
Future<PrepareReceiveResponse> prepareReceivePayment({required PrepareReceiveRequest req, dynamic hint}); Future<PrepareReceiveResponse> prepareReceivePayment({required PrepareReceiveRequest req, dynamic hint});
@@ -183,19 +183,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
@override @override
Future<List<Payment>> listPayments({required bool withScan, required bool includePending, dynamic hint}) { Future<List<Payment>> listPayments({dynamic hint}) {
return handler.executeNormal(NormalTask( return handler.executeNormal(NormalTask(
callFfi: (port_) { callFfi: (port_) {
var arg0 = cst_encode_bool(withScan); return wire.wire_list_payments(port_);
var arg1 = cst_encode_bool(includePending);
return wire.wire_list_payments(port_, arg0, arg1);
}, },
codec: DcoCodec( codec: DcoCodec(
decodeSuccessData: dco_decode_list_payment, decodeSuccessData: dco_decode_list_payment,
decodeErrorData: dco_decode_AnyhowException, decodeErrorData: dco_decode_AnyhowException,
), ),
constMeta: kListPaymentsConstMeta, constMeta: kListPaymentsConstMeta,
argValues: [withScan, includePending], argValues: [],
apiImpl: this, apiImpl: this,
hint: hint, hint: hint,
)); ));
@@ -203,7 +201,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kListPaymentsConstMeta => const TaskConstMeta( TaskConstMeta get kListPaymentsConstMeta => const TaskConstMeta(
debugName: "list_payments", debugName: "list_payments",
argNames: ["withScan", "includePending"], argNames: [],
); );
@override @override
@@ -381,12 +379,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_restore_request(raw); return dco_decode_restore_request(raw);
} }
@protected
int dco_decode_box_autoadd_u_32(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return raw as int;
}
@protected @protected
int dco_decode_box_autoadd_u_64(dynamic raw) { int dco_decode_box_autoadd_u_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -419,10 +411,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
GetInfoResponse dco_decode_get_info_response(dynamic raw) { GetInfoResponse dco_decode_get_info_response(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); if (arr.length != 4) throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
return GetInfoResponse( return GetInfoResponse(
balanceSat: dco_decode_u_64(arr[0]), balanceSat: dco_decode_u_64(arr[0]),
pubkey: dco_decode_String(arr[1]), pendingSendSat: dco_decode_u_64(arr[1]),
pendingReceiveSat: dco_decode_u_64(arr[2]),
pubkey: dco_decode_String(arr[3]),
); );
} }
@@ -451,15 +445,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
@protected @protected
String? dco_decode_opt_String(dynamic raw) { NewSwapState dco_decode_new_swap_state(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null ? null : dco_decode_String(raw); return NewSwapState.values[raw as int];
} }
@protected @protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw) { String? dco_decode_opt_String(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
return raw == null ? null : dco_decode_box_autoadd_u_32(raw); return raw == null ? null : dco_decode_String(raw);
} }
@protected @protected
@@ -472,14 +466,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
Payment dco_decode_payment(dynamic raw) { Payment dco_decode_payment(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>; final arr = raw as List<dynamic>;
if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}');
return Payment( return Payment(
id: dco_decode_opt_String(arr[0]), txId: dco_decode_String(arr[0]),
timestamp: dco_decode_opt_box_autoadd_u_32(arr[1]), swapId: dco_decode_opt_String(arr[1]),
amountSat: dco_decode_u_64(arr[2]), timestamp: dco_decode_u_32(arr[2]),
feesSat: dco_decode_opt_box_autoadd_u_64(arr[3]), amountSat: dco_decode_u_64(arr[3]),
paymentType: dco_decode_payment_type(arr[4]), feesSat: dco_decode_opt_box_autoadd_u_64(arr[4]),
invoice: dco_decode_opt_String(arr[5]), paymentType: dco_decode_payment_type(arr[5]),
status: dco_decode_new_swap_state(arr[6]),
); );
} }
@@ -514,7 +509,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
case 10: case 10:
return PaymentError_Refunded( return PaymentError_Refunded(
err: dco_decode_String(raw[1]), err: dco_decode_String(raw[1]),
txid: dco_decode_String(raw[2]), refundTxId: dco_decode_String(raw[2]),
); );
case 11: case 11:
return PaymentError_SendError( return PaymentError_SendError(
@@ -694,12 +689,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_restore_request(deserializer)); return (sse_decode_restore_request(deserializer));
} }
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_u_32(deserializer));
}
@protected @protected
int sse_decode_box_autoadd_u_64(SseDeserializer deserializer) { int sse_decode_box_autoadd_u_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -726,8 +715,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
GetInfoResponse sse_decode_get_info_response(SseDeserializer deserializer) { GetInfoResponse sse_decode_get_info_response(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
var var_balanceSat = sse_decode_u_64(deserializer); var var_balanceSat = sse_decode_u_64(deserializer);
var var_pendingSendSat = sse_decode_u_64(deserializer);
var var_pendingReceiveSat = sse_decode_u_64(deserializer);
var var_pubkey = sse_decode_String(deserializer); var var_pubkey = sse_decode_String(deserializer);
return GetInfoResponse(balanceSat: var_balanceSat, pubkey: var_pubkey); return GetInfoResponse(
balanceSat: var_balanceSat,
pendingSendSat: var_pendingSendSat,
pendingReceiveSat: var_pendingReceiveSat,
pubkey: var_pubkey);
} }
@protected @protected
@@ -762,6 +757,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return Network.values[inner]; return Network.values[inner];
} }
@protected
NewSwapState sse_decode_new_swap_state(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var inner = sse_decode_i_32(deserializer);
return NewSwapState.values[inner];
}
@protected @protected
String? sse_decode_opt_String(SseDeserializer deserializer) { String? sse_decode_opt_String(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -773,17 +775,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
if (sse_decode_bool(deserializer)) {
return (sse_decode_box_autoadd_u_32(deserializer));
} else {
return null;
}
}
@protected @protected
int? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer) { int? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -798,19 +789,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected @protected
Payment sse_decode_payment(SseDeserializer deserializer) { Payment sse_decode_payment(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
var var_id = sse_decode_opt_String(deserializer); var var_txId = sse_decode_String(deserializer);
var var_timestamp = sse_decode_opt_box_autoadd_u_32(deserializer); var var_swapId = sse_decode_opt_String(deserializer);
var var_timestamp = sse_decode_u_32(deserializer);
var var_amountSat = sse_decode_u_64(deserializer); var var_amountSat = sse_decode_u_64(deserializer);
var var_feesSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_feesSat = sse_decode_opt_box_autoadd_u_64(deserializer);
var var_paymentType = sse_decode_payment_type(deserializer); var var_paymentType = sse_decode_payment_type(deserializer);
var var_invoice = sse_decode_opt_String(deserializer); var var_status = sse_decode_new_swap_state(deserializer);
return Payment( return Payment(
id: var_id, txId: var_txId,
swapId: var_swapId,
timestamp: var_timestamp, timestamp: var_timestamp,
amountSat: var_amountSat, amountSat: var_amountSat,
feesSat: var_feesSat, feesSat: var_feesSat,
paymentType: var_paymentType, paymentType: var_paymentType,
invoice: var_invoice); status: var_status);
} }
@protected @protected
@@ -843,8 +836,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return PaymentError_PersistError(); return PaymentError_PersistError();
case 10: case 10:
var var_err = sse_decode_String(deserializer); var var_err = sse_decode_String(deserializer);
var var_txid = sse_decode_String(deserializer); var var_refundTxId = sse_decode_String(deserializer);
return PaymentError_Refunded(err: var_err, txid: var_txid); return PaymentError_Refunded(err: var_err, refundTxId: var_refundTxId);
case 11: case 11:
var var_err = sse_decode_String(deserializer); var var_err = sse_decode_String(deserializer);
return PaymentError_SendError(err: var_err); return PaymentError_SendError(err: var_err);
@@ -956,6 +949,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return cst_encode_i_32(raw.index); return cst_encode_i_32(raw.index);
} }
@protected
int cst_encode_new_swap_state(NewSwapState raw) {
// Codec=Cst (C-struct based), see doc to use other codecs
return cst_encode_i_32(raw.index);
}
@protected @protected
int cst_encode_payment_type(PaymentType raw) { int cst_encode_payment_type(PaymentType raw) {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
@@ -1041,12 +1040,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_restore_request(self, serializer); sse_encode_restore_request(self, serializer);
} }
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_32(self, serializer);
}
@protected @protected
void sse_encode_box_autoadd_u_64(int self, SseSerializer serializer) { void sse_encode_box_autoadd_u_64(int self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -1071,6 +1064,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
void sse_encode_get_info_response(GetInfoResponse self, SseSerializer serializer) { void sse_encode_get_info_response(GetInfoResponse self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_64(self.balanceSat, serializer); sse_encode_u_64(self.balanceSat, serializer);
sse_encode_u_64(self.pendingSendSat, serializer);
sse_encode_u_64(self.pendingReceiveSat, serializer);
sse_encode_String(self.pubkey, serializer); sse_encode_String(self.pubkey, serializer);
} }
@@ -1102,6 +1097,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(self.index, serializer); sse_encode_i_32(self.index, serializer);
} }
@protected
void sse_encode_new_swap_state(NewSwapState self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_i_32(self.index, serializer);
}
@protected @protected
void sse_encode_opt_String(String? self, SseSerializer serializer) { void sse_encode_opt_String(String? self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -1112,16 +1113,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
} }
} }
@protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_bool(self != null, serializer);
if (self != null) {
sse_encode_box_autoadd_u_32(self, serializer);
}
}
@protected @protected
void sse_encode_opt_box_autoadd_u_64(int? self, SseSerializer serializer) { void sse_encode_opt_box_autoadd_u_64(int? self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -1135,12 +1126,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected @protected
void sse_encode_payment(Payment self, SseSerializer serializer) { void sse_encode_payment(Payment self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_opt_String(self.id, serializer); sse_encode_String(self.txId, serializer);
sse_encode_opt_box_autoadd_u_32(self.timestamp, serializer); sse_encode_opt_String(self.swapId, serializer);
sse_encode_u_32(self.timestamp, serializer);
sse_encode_u_64(self.amountSat, serializer); sse_encode_u_64(self.amountSat, serializer);
sse_encode_opt_box_autoadd_u_64(self.feesSat, serializer); sse_encode_opt_box_autoadd_u_64(self.feesSat, serializer);
sse_encode_payment_type(self.paymentType, serializer); sse_encode_payment_type(self.paymentType, serializer);
sse_encode_opt_String(self.invoice, serializer); sse_encode_new_swap_state(self.status, serializer);
} }
@protected @protected
@@ -1169,10 +1161,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_i_32(8, serializer); sse_encode_i_32(8, serializer);
case PaymentError_PersistError(): case PaymentError_PersistError():
sse_encode_i_32(9, serializer); sse_encode_i_32(9, serializer);
case PaymentError_Refunded(err: final err, txid: final txid): case PaymentError_Refunded(err: final err, refundTxId: final refundTxId):
sse_encode_i_32(10, serializer); sse_encode_i_32(10, serializer);
sse_encode_String(err, serializer); sse_encode_String(err, serializer);
sse_encode_String(txid, serializer); sse_encode_String(refundTxId, serializer);
case PaymentError_SendError(err: final err): case PaymentError_SendError(err: final err):
sse_encode_i_32(11, serializer); sse_encode_i_32(11, serializer);
sse_encode_String(err, serializer); sse_encode_String(err, serializer);

View File

@@ -50,9 +50,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
RestoreRequest dco_decode_box_autoadd_restore_request(dynamic raw); RestoreRequest dco_decode_box_autoadd_restore_request(dynamic raw);
@protected
int dco_decode_box_autoadd_u_32(dynamic raw);
@protected @protected
int dco_decode_box_autoadd_u_64(dynamic raw); int dco_decode_box_autoadd_u_64(dynamic raw);
@@ -78,10 +75,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
Network dco_decode_network(dynamic raw); Network dco_decode_network(dynamic raw);
@protected @protected
String? dco_decode_opt_String(dynamic raw); NewSwapState dco_decode_new_swap_state(dynamic raw);
@protected @protected
int? dco_decode_opt_box_autoadd_u_32(dynamic raw); String? dco_decode_opt_String(dynamic raw);
@protected @protected
int? dco_decode_opt_box_autoadd_u_64(dynamic raw); int? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@@ -158,9 +155,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
RestoreRequest sse_decode_box_autoadd_restore_request(SseDeserializer deserializer); RestoreRequest sse_decode_box_autoadd_restore_request(SseDeserializer deserializer);
@protected
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
@protected @protected
int sse_decode_box_autoadd_u_64(SseDeserializer deserializer); int sse_decode_box_autoadd_u_64(SseDeserializer deserializer);
@@ -186,10 +180,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
Network sse_decode_network(SseDeserializer deserializer); Network sse_decode_network(SseDeserializer deserializer);
@protected @protected
String? sse_decode_opt_String(SseDeserializer deserializer); NewSwapState sse_decode_new_swap_state(SseDeserializer deserializer);
@protected @protected
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); String? sse_decode_opt_String(SseDeserializer deserializer);
@protected @protected
int? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); int? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@@ -308,12 +302,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
return ptr; return ptr;
} }
@protected
ffi.Pointer<ffi.Uint32> cst_encode_box_autoadd_u_32(int raw) {
// Codec=Cst (C-struct based), see doc to use other codecs
return wire.cst_new_box_autoadd_u_32(cst_encode_u_32(raw));
}
@protected @protected
ffi.Pointer<ffi.Uint64> cst_encode_box_autoadd_u_64(int raw) { ffi.Pointer<ffi.Uint64> cst_encode_box_autoadd_u_64(int raw) {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
@@ -344,12 +332,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
return raw == null ? ffi.nullptr : cst_encode_String(raw); return raw == null ? ffi.nullptr : cst_encode_String(raw);
} }
@protected
ffi.Pointer<ffi.Uint32> cst_encode_opt_box_autoadd_u_32(int? raw) {
// Codec=Cst (C-struct based), see doc to use other codecs
return raw == null ? ffi.nullptr : cst_encode_box_autoadd_u_32(raw);
}
@protected @protected
ffi.Pointer<ffi.Uint64> cst_encode_opt_box_autoadd_u_64(int? raw) { ffi.Pointer<ffi.Uint64> cst_encode_opt_box_autoadd_u_64(int? raw) {
// Codec=Cst (C-struct based), see doc to use other codecs // Codec=Cst (C-struct based), see doc to use other codecs
@@ -419,17 +401,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void cst_api_fill_to_wire_get_info_response(GetInfoResponse apiObj, wire_cst_get_info_response wireObj) { void cst_api_fill_to_wire_get_info_response(GetInfoResponse apiObj, wire_cst_get_info_response wireObj) {
wireObj.balance_sat = cst_encode_u_64(apiObj.balanceSat); wireObj.balance_sat = cst_encode_u_64(apiObj.balanceSat);
wireObj.pending_send_sat = cst_encode_u_64(apiObj.pendingSendSat);
wireObj.pending_receive_sat = cst_encode_u_64(apiObj.pendingReceiveSat);
wireObj.pubkey = cst_encode_String(apiObj.pubkey); wireObj.pubkey = cst_encode_String(apiObj.pubkey);
} }
@protected @protected
void cst_api_fill_to_wire_payment(Payment apiObj, wire_cst_payment wireObj) { void cst_api_fill_to_wire_payment(Payment apiObj, wire_cst_payment wireObj) {
wireObj.id = cst_encode_opt_String(apiObj.id); wireObj.tx_id = cst_encode_String(apiObj.txId);
wireObj.timestamp = cst_encode_opt_box_autoadd_u_32(apiObj.timestamp); wireObj.swap_id = cst_encode_opt_String(apiObj.swapId);
wireObj.timestamp = cst_encode_u_32(apiObj.timestamp);
wireObj.amount_sat = cst_encode_u_64(apiObj.amountSat); wireObj.amount_sat = cst_encode_u_64(apiObj.amountSat);
wireObj.fees_sat = cst_encode_opt_box_autoadd_u_64(apiObj.feesSat); wireObj.fees_sat = cst_encode_opt_box_autoadd_u_64(apiObj.feesSat);
wireObj.payment_type = cst_encode_payment_type(apiObj.paymentType); wireObj.payment_type = cst_encode_payment_type(apiObj.paymentType);
wireObj.invoice = cst_encode_opt_String(apiObj.invoice); wireObj.status = cst_encode_new_swap_state(apiObj.status);
} }
@protected @protected
@@ -480,10 +465,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
} }
if (apiObj is PaymentError_Refunded) { if (apiObj is PaymentError_Refunded) {
var pre_err = cst_encode_String(apiObj.err); var pre_err = cst_encode_String(apiObj.err);
var pre_txid = cst_encode_String(apiObj.txid); var pre_refund_tx_id = cst_encode_String(apiObj.refundTxId);
wireObj.tag = 10; wireObj.tag = 10;
wireObj.kind.Refunded.err = pre_err; wireObj.kind.Refunded.err = pre_err;
wireObj.kind.Refunded.txid = pre_txid; wireObj.kind.Refunded.refund_tx_id = pre_refund_tx_id;
return; return;
} }
if (apiObj is PaymentError_SendError) { if (apiObj is PaymentError_SendError) {
@@ -553,6 +538,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
int cst_encode_network(Network raw); int cst_encode_network(Network raw);
@protected
int cst_encode_new_swap_state(NewSwapState raw);
@protected @protected
int cst_encode_payment_type(PaymentType raw); int cst_encode_payment_type(PaymentType raw);
@@ -595,9 +583,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_box_autoadd_restore_request(RestoreRequest self, SseSerializer serializer); void sse_encode_box_autoadd_restore_request(RestoreRequest self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
@protected @protected
void sse_encode_box_autoadd_u_64(int self, SseSerializer serializer); void sse_encode_box_autoadd_u_64(int self, SseSerializer serializer);
@@ -623,10 +608,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
void sse_encode_network(Network self, SseSerializer serializer); void sse_encode_network(Network self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_String(String? self, SseSerializer serializer); void sse_encode_new_swap_state(NewSwapState self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); void sse_encode_opt_String(String? self, SseSerializer serializer);
@protected @protected
void sse_encode_opt_box_autoadd_u_64(int? self, SseSerializer serializer); void sse_encode_opt_box_autoadd_u_64(int? self, SseSerializer serializer);
@@ -767,20 +752,15 @@ class RustLibWire implements BaseWire {
void wire_list_payments( void wire_list_payments(
int port_, int port_,
bool with_scan,
bool include_pending,
) { ) {
return _wire_list_payments( return _wire_list_payments(
port_, port_,
with_scan,
include_pending,
); );
} }
late final _wire_list_paymentsPtr = late final _wire_list_paymentsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Bool, ffi.Bool)>>( _lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>('frbgen_breez_liquid_wire_list_payments');
'frbgen_breez_liquid_wire_list_payments'); late final _wire_list_payments = _wire_list_paymentsPtr.asFunction<void Function(int)>();
late final _wire_list_payments = _wire_list_paymentsPtr.asFunction<void Function(int, bool, bool)>();
void wire_prepare_receive_payment( void wire_prepare_receive_payment(
int port_, int port_,
@@ -932,20 +912,6 @@ class RustLibWire implements BaseWire {
late final _cst_new_box_autoadd_restore_request = late final _cst_new_box_autoadd_restore_request =
_cst_new_box_autoadd_restore_requestPtr.asFunction<ffi.Pointer<wire_cst_restore_request> Function()>(); _cst_new_box_autoadd_restore_requestPtr.asFunction<ffi.Pointer<wire_cst_restore_request> Function()>();
ffi.Pointer<ffi.Uint32> cst_new_box_autoadd_u_32(
int value,
) {
return _cst_new_box_autoadd_u_32(
value,
);
}
late final _cst_new_box_autoadd_u_32Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Uint32> Function(ffi.Uint32)>>(
'frbgen_breez_liquid_cst_new_box_autoadd_u_32');
late final _cst_new_box_autoadd_u_32 =
_cst_new_box_autoadd_u_32Ptr.asFunction<ffi.Pointer<ffi.Uint32> Function(int)>();
ffi.Pointer<ffi.Uint64> cst_new_box_autoadd_u_64( ffi.Pointer<ffi.Uint64> cst_new_box_autoadd_u_64(
int value, int value,
) { ) {
@@ -1055,9 +1021,12 @@ final class wire_cst_prepare_send_response extends ffi.Struct {
} }
final class wire_cst_payment extends ffi.Struct { final class wire_cst_payment extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> id; external ffi.Pointer<wire_cst_list_prim_u_8_strict> tx_id;
external ffi.Pointer<ffi.Uint32> timestamp; external ffi.Pointer<wire_cst_list_prim_u_8_strict> swap_id;
@ffi.Uint32()
external int timestamp;
@ffi.Uint64() @ffi.Uint64()
external int amount_sat; external int amount_sat;
@@ -1067,7 +1036,8 @@ final class wire_cst_payment extends ffi.Struct {
@ffi.Int32() @ffi.Int32()
external int payment_type; external int payment_type;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> invoice; @ffi.Int32()
external int status;
} }
final class wire_cst_list_payment extends ffi.Struct { final class wire_cst_list_payment extends ffi.Struct {
@@ -1081,6 +1051,12 @@ final class wire_cst_get_info_response extends ffi.Struct {
@ffi.Uint64() @ffi.Uint64()
external int balance_sat; external int balance_sat;
@ffi.Uint64()
external int pending_send_sat;
@ffi.Uint64()
external int pending_receive_sat;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> pubkey; external ffi.Pointer<wire_cst_list_prim_u_8_strict> pubkey;
} }
@@ -1095,7 +1071,7 @@ final class wire_cst_PaymentError_LwkError extends ffi.Struct {
final class wire_cst_PaymentError_Refunded extends ffi.Struct { final class wire_cst_PaymentError_Refunded extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> err; external ffi.Pointer<wire_cst_list_prim_u_8_strict> err;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> txid; external ffi.Pointer<wire_cst_list_prim_u_8_strict> refund_tx_id;
} }
final class wire_cst_PaymentError_SendError extends ffi.Struct { final class wire_cst_PaymentError_SendError extends ffi.Struct {

View File

@@ -47,16 +47,26 @@ class GetInfoRequest {
} }
class GetInfoResponse { class GetInfoResponse {
/// Usable balance. This is the confirmed onchain balance minus `pending_send_sat`.
final int balanceSat; final int balanceSat;
/// Amount that is being used for ongoing Send swaps
final int pendingSendSat;
/// Incoming amount that is pending from ongoing Receive swaps
final int pendingReceiveSat;
final String pubkey; final String pubkey;
const GetInfoResponse({ const GetInfoResponse({
required this.balanceSat, required this.balanceSat,
required this.pendingSendSat,
required this.pendingReceiveSat,
required this.pubkey, required this.pubkey,
}); });
@override @override
int get hashCode => balanceSat.hashCode ^ pubkey.hashCode; int get hashCode =>
balanceSat.hashCode ^ pendingSendSat.hashCode ^ pendingReceiveSat.hashCode ^ pubkey.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -64,6 +74,8 @@ class GetInfoResponse {
other is GetInfoResponse && other is GetInfoResponse &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
balanceSat == other.balanceSat && balanceSat == other.balanceSat &&
pendingSendSat == other.pendingSendSat &&
pendingReceiveSat == other.pendingReceiveSat &&
pubkey == other.pubkey; pubkey == other.pubkey;
} }
@@ -73,50 +85,86 @@ enum Network {
; ;
} }
enum NewSwapState {
created,
pending,
complete,
failed,
;
}
/// Represents an SDK payment.
///
/// By default, this is an onchain tx. It may represent a swap, if swap metadata is available.
class Payment { class Payment {
final String? id; /// The tx ID of the onchain transaction
final int? timestamp; final String txId;
/// The swap ID, if any swap is associated with this payment
final String? swapId;
/// Composite timestamp that can be used for sorting or displaying the payment.
///
/// If this payment has an associated swap, it is the swap creation time. Otherwise, the point
/// in time when the underlying tx was included in a block. If there is no associated swap
/// available and the underlying tx is not yet confirmed, the value is `now()`.
final int timestamp;
/// The payment amount, which corresponds to the onchain tx amount.
///
/// In case of an outbound payment (Send), this is the payer amount. Otherwise it's the receiver amount.
final int amountSat; final int amountSat;
/// If a swap is associated with this payment, this represents the total fees paid by the
/// sender. In other words, it's the delta between the amount that was sent and the amount
/// received.
final int? feesSat; final int? feesSat;
final PaymentType paymentType; final PaymentType paymentType;
final String? invoice;
/// Composite status representing the overall status of the payment.
///
/// If the tx has no associated swap, this reflects the onchain tx status (confirmed or not).
///
/// If the tx has an associated swap, this is determined by the swap status (pending or complete).
final NewSwapState status;
const Payment({ const Payment({
this.id, required this.txId,
this.timestamp, this.swapId,
required this.timestamp,
required this.amountSat, required this.amountSat,
this.feesSat, this.feesSat,
required this.paymentType, required this.paymentType,
this.invoice, required this.status,
}); });
@override @override
int get hashCode => int get hashCode =>
id.hashCode ^ txId.hashCode ^
swapId.hashCode ^
timestamp.hashCode ^ timestamp.hashCode ^
amountSat.hashCode ^ amountSat.hashCode ^
feesSat.hashCode ^ feesSat.hashCode ^
paymentType.hashCode ^ paymentType.hashCode ^
invoice.hashCode; status.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is Payment && other is Payment &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
id == other.id && txId == other.txId &&
swapId == other.swapId &&
timestamp == other.timestamp && timestamp == other.timestamp &&
amountSat == other.amountSat && amountSat == other.amountSat &&
feesSat == other.feesSat && feesSat == other.feesSat &&
paymentType == other.paymentType && paymentType == other.paymentType &&
invoice == other.invoice; status == other.status;
} }
enum PaymentType { enum PaymentType {
sent, receive,
received, send,
pendingReceive,
pendingSend,
; ;
} }

View File

@@ -276,20 +276,6 @@ class FlutterBreezLiquidBindings {
_frbgen_breez_liquid_cst_new_box_autoadd_restore_requestPtr _frbgen_breez_liquid_cst_new_box_autoadd_restore_requestPtr
.asFunction<ffi.Pointer<wire_cst_restore_request> Function()>(); .asFunction<ffi.Pointer<wire_cst_restore_request> Function()>();
ffi.Pointer<ffi.Uint32> frbgen_breez_liquid_cst_new_box_autoadd_u_32(
int value,
) {
return _frbgen_breez_liquid_cst_new_box_autoadd_u_32(
value,
);
}
late final _frbgen_breez_liquid_cst_new_box_autoadd_u_32Ptr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Uint32> Function(ffi.Uint32)>>(
'frbgen_breez_liquid_cst_new_box_autoadd_u_32');
late final _frbgen_breez_liquid_cst_new_box_autoadd_u_32 =
_frbgen_breez_liquid_cst_new_box_autoadd_u_32Ptr.asFunction<ffi.Pointer<ffi.Uint32> Function(int)>();
ffi.Pointer<ffi.Uint64> frbgen_breez_liquid_cst_new_box_autoadd_u_64( ffi.Pointer<ffi.Uint64> frbgen_breez_liquid_cst_new_box_autoadd_u_64(
int value, int value,
) { ) {
@@ -411,9 +397,12 @@ final class wire_cst_prepare_send_response extends ffi.Struct {
} }
final class wire_cst_payment extends ffi.Struct { final class wire_cst_payment extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> id; external ffi.Pointer<wire_cst_list_prim_u_8_strict> tx_id;
external ffi.Pointer<ffi.Uint32> timestamp; external ffi.Pointer<wire_cst_list_prim_u_8_strict> swap_id;
@ffi.Uint32()
external int timestamp;
@ffi.Uint64() @ffi.Uint64()
external int amount_sat; external int amount_sat;
@@ -423,7 +412,8 @@ final class wire_cst_payment extends ffi.Struct {
@ffi.Int32() @ffi.Int32()
external int payment_type; external int payment_type;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> invoice; @ffi.Int32()
external int status;
} }
final class wire_cst_list_payment extends ffi.Struct { final class wire_cst_list_payment extends ffi.Struct {
@@ -437,6 +427,12 @@ final class wire_cst_get_info_response extends ffi.Struct {
@ffi.Uint64() @ffi.Uint64()
external int balance_sat; external int balance_sat;
@ffi.Uint64()
external int pending_send_sat;
@ffi.Uint64()
external int pending_receive_sat;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> pubkey; external ffi.Pointer<wire_cst_list_prim_u_8_strict> pubkey;
} }
@@ -451,7 +447,7 @@ final class wire_cst_PaymentError_LwkError extends ffi.Struct {
final class wire_cst_PaymentError_Refunded extends ffi.Struct { final class wire_cst_PaymentError_Refunded extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> err; external ffi.Pointer<wire_cst_list_prim_u_8_strict> err;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> txid; external ffi.Pointer<wire_cst_list_prim_u_8_strict> refund_tx_id;
} }
final class wire_cst_PaymentError_SendError extends ffi.Struct { final class wire_cst_PaymentError_SendError extends ffi.Struct {

View File

@@ -81,6 +81,8 @@ fun asGetInfoResponse(getInfoResponse: ReadableMap): GetInfoResponse? {
getInfoResponse, getInfoResponse,
arrayOf( arrayOf(
"balanceSat", "balanceSat",
"pendingSendSat",
"pendingReceiveSat",
"pubkey", "pubkey",
), ),
) )
@@ -88,9 +90,13 @@ fun asGetInfoResponse(getInfoResponse: ReadableMap): GetInfoResponse? {
return null return null
} }
val balanceSat = getInfoResponse.getDouble("balanceSat").toULong() val balanceSat = getInfoResponse.getDouble("balanceSat").toULong()
val pendingSendSat = getInfoResponse.getDouble("pendingSendSat").toULong()
val pendingReceiveSat = getInfoResponse.getDouble("pendingReceiveSat").toULong()
val pubkey = getInfoResponse.getString("pubkey")!! val pubkey = getInfoResponse.getString("pubkey")!!
return GetInfoResponse( return GetInfoResponse(
balanceSat, balanceSat,
pendingSendSat,
pendingReceiveSat,
pubkey, pubkey,
) )
} }
@@ -98,6 +104,8 @@ fun asGetInfoResponse(getInfoResponse: ReadableMap): GetInfoResponse? {
fun readableMapOf(getInfoResponse: GetInfoResponse): ReadableMap { fun readableMapOf(getInfoResponse: GetInfoResponse): ReadableMap {
return readableMapOf( return readableMapOf(
"balanceSat" to getInfoResponse.balanceSat, "balanceSat" to getInfoResponse.balanceSat,
"pendingSendSat" to getInfoResponse.pendingSendSat,
"pendingReceiveSat" to getInfoResponse.pendingReceiveSat,
"pubkey" to getInfoResponse.pubkey, "pubkey" to getInfoResponse.pubkey,
) )
} }

View File

@@ -161,6 +161,18 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext
} }
} }
@ReactMethod
fun sync(promise: Promise) {
executor.execute {
try {
getBindingLiquidSdk().sync()
promise.resolve(readableMapOf("status" to "ok"))
} catch (e: Exception) {
promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e)
}
}
}
@ReactMethod @ReactMethod
fun backup(promise: Promise) { fun backup(promise: Promise) {
executor.execute { executor.execute {

View File

@@ -87,12 +87,20 @@ enum BreezLiquidSDKMapper {
guard let balanceSat = getInfoResponse["balanceSat"] as? UInt64 else { guard let balanceSat = getInfoResponse["balanceSat"] as? UInt64 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "balanceSat", typeName: "GetInfoResponse")) throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "balanceSat", typeName: "GetInfoResponse"))
} }
guard let pendingSendSat = getInfoResponse["pendingSendSat"] as? UInt64 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "pendingSendSat", typeName: "GetInfoResponse"))
}
guard let pendingReceiveSat = getInfoResponse["pendingReceiveSat"] as? UInt64 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "pendingReceiveSat", typeName: "GetInfoResponse"))
}
guard let pubkey = getInfoResponse["pubkey"] as? String else { guard let pubkey = getInfoResponse["pubkey"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "pubkey", typeName: "GetInfoResponse")) throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "pubkey", typeName: "GetInfoResponse"))
} }
return GetInfoResponse( return GetInfoResponse(
balanceSat: balanceSat, balanceSat: balanceSat,
pendingSendSat: pendingSendSat,
pendingReceiveSat: pendingReceiveSat,
pubkey: pubkey pubkey: pubkey
) )
} }
@@ -100,6 +108,8 @@ enum BreezLiquidSDKMapper {
static func dictionaryOf(getInfoResponse: GetInfoResponse) -> [String: Any?] { static func dictionaryOf(getInfoResponse: GetInfoResponse) -> [String: Any?] {
return [ return [
"balanceSat": getInfoResponse.balanceSat, "balanceSat": getInfoResponse.balanceSat,
"pendingSendSat": getInfoResponse.pendingSendSat,
"pendingReceiveSat": getInfoResponse.pendingReceiveSat,
"pubkey": getInfoResponse.pubkey, "pubkey": getInfoResponse.pubkey,
] ]
} }

View File

@@ -39,6 +39,11 @@ RCT_EXTERN_METHOD(
reject: (RCTPromiseRejectBlock)reject reject: (RCTPromiseRejectBlock)reject
) )
RCT_EXTERN_METHOD(
sync: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD( RCT_EXTERN_METHOD(
backup: (RCTPromiseResolveBlock)resolve backup: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject reject: (RCTPromiseRejectBlock)reject

View File

@@ -123,6 +123,16 @@ class RNBreezLiquidSDK: RCTEventEmitter {
} }
} }
@objc(sync:reject:)
func sync(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {
try getBindingLiquidSdk().sync()
resolve(["status": "ok"])
} catch let err {
rejectErr(err: err, reject: reject)
}
}
@objc(backup:reject:) @objc(backup:reject:)
func backup(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { func backup(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do { do {

View File

@@ -29,6 +29,8 @@ export interface GetInfoRequest {
export interface GetInfoResponse { export interface GetInfoResponse {
balanceSat: number balanceSat: number
pendingSendSat: number
pendingReceiveSat: number
pubkey: string pubkey: string
} }
@@ -98,6 +100,10 @@ export const receivePayment = async (req: PrepareReceiveResponse): Promise<Recei
return response return response
} }
export const sync = async (): Promise<void> => {
await BreezLiquidSDK.sync()
}
export const backup = async (): Promise<void> => { export const backup = async (): Promise<void> => {
await BreezLiquidSDK.backup() await BreezLiquidSDK.backup()
} }