feat: add Esplora client to chain services (#761)

Co-authored-by: Daniel Granhão <32176319+danielgranhao@users.noreply.github.com>
This commit is contained in:
yse
2025-03-27 10:40:10 +01:00
committed by GitHub
parent ecd7c30d39
commit 10e3ab71e0
51 changed files with 2474 additions and 1056 deletions

178
cli/Cargo.lock generated
View File

@@ -412,6 +412,12 @@ dependencies = [
"bitcoin_hashes 0.14.0", "bitcoin_hashes 0.14.0",
] ]
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@@ -685,6 +691,7 @@ dependencies = [
"ecies", "ecies",
"electrum-client", "electrum-client",
"env_logger 0.11.7", "env_logger 0.11.7",
"esplora-client",
"flutter_rust_bridge", "flutter_rust_bridge",
"futures-util", "futures-util",
"glob", "glob",
@@ -1320,6 +1327,20 @@ version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f"
[[package]]
name = "esplora-client"
version = "0.11.0"
source = "git+https://github.com/hydra-yse/rust-esplora-client?branch=scripthash-utxo#513fb83a872425a69252e12db2f84d96973d08a2"
dependencies = [
"bitcoin 0.32.5",
"hex-conservative 0.2.1",
"log",
"minreq",
"reqwest 0.11.27",
"serde",
"tokio",
]
[[package]] [[package]]
name = "fallible-iterator" name = "fallible-iterator"
version = "0.3.0" version = "0.3.0"
@@ -1993,6 +2014,20 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.5" version = "0.27.5"
@@ -2023,6 +2058,19 @@ dependencies = [
"tokio-io-timeout", "tokio-io-timeout",
] ]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.6.0" version = "0.6.0"
@@ -2677,8 +2725,7 @@ dependencies = [
[[package]] [[package]]
name = "lwk_wollet" name = "lwk_wollet"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/breez/lwk?rev=0b18e777d496#0b18e777d496bd3ee0602b49b838d3d293e43e23"
checksum = "44164918e75771585624098a328cf0d5e28931ccdd5af1c9a95a6ecf0c5cb67a"
dependencies = [ dependencies = [
"aes-gcm-siv", "aes-gcm-siv",
"age", "age",
@@ -2790,6 +2837,7 @@ version = "2.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567496f13503d6cae8c9f961f34536850275f396307d7a6b981eef1464032f53" checksum = "567496f13503d6cae8c9f961f34536850275f396307d7a6b981eef1464032f53"
dependencies = [ dependencies = [
"base64 0.12.3",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
@@ -3621,6 +3669,51 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-rustls 0.24.2",
"hyper-tls 0.5.0",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tokio-socks",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.25.4",
"winreg",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.12" version = "0.12.12"
@@ -3636,8 +3729,8 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-rustls", "hyper-rustls 0.27.5",
"hyper-tls", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
@@ -3652,7 +3745,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tower 0.5.2", "tower 0.5.2",
@@ -3681,8 +3774,8 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-rustls", "hyper-rustls 0.27.5",
"hyper-tls", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
@@ -3700,7 +3793,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls 0.26.2", "tokio-rustls 0.26.2",
@@ -3884,6 +3977,18 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring 0.17.14",
"rustls-webpki 0.101.7",
"sct",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.23" version = "0.23.23"
@@ -3894,7 +3999,7 @@ dependencies = [
"once_cell", "once_cell",
"ring 0.17.14", "ring 0.17.14",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki 0.102.8",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -3938,6 +4043,16 @@ dependencies = [
"web-time", "web-time",
] ]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring 0.17.14",
"untrusted 0.9.0",
]
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.102.8" version = "0.102.8"
@@ -4423,6 +4538,17 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys 0.5.0",
]
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.6.1" version = "0.6.1"
@@ -4431,7 +4557,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys 0.6.0",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
] ]
[[package]] [[package]]
@@ -4636,6 +4772,16 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.2" version = "0.26.2"
@@ -4646,6 +4792,18 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror 1.0.69",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.17" version = "0.1.17"

178
lib/Cargo.lock generated
View File

@@ -492,6 +492,12 @@ dependencies = [
"bitcoin_hashes 0.14.0", "bitcoin_hashes 0.14.0",
] ]
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.1" version = "0.13.1"
@@ -780,6 +786,7 @@ dependencies = [
"ecies", "ecies",
"electrum-client", "electrum-client",
"env_logger 0.11.7", "env_logger 0.11.7",
"esplora-client",
"flutter_rust_bridge", "flutter_rust_bridge",
"futures-util", "futures-util",
"getrandom 0.2.14", "getrandom 0.2.14",
@@ -1505,6 +1512,20 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "esplora-client"
version = "0.11.0"
source = "git+https://github.com/hydra-yse/rust-esplora-client?branch=scripthash-utxo#513fb83a872425a69252e12db2f84d96973d08a2"
dependencies = [
"bitcoin 0.32.5",
"hex-conservative 0.2.1",
"log",
"minreq",
"reqwest 0.11.27",
"serde",
"tokio",
]
[[package]] [[package]]
name = "fallible-iterator" name = "fallible-iterator"
version = "0.3.0" version = "0.3.0"
@@ -2219,6 +2240,20 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.32",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.5" version = "0.27.5"
@@ -2249,6 +2284,19 @@ dependencies = [
"tokio-io-timeout", "tokio-io-timeout",
] ]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.32",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]] [[package]]
name = "hyper-tls" name = "hyper-tls"
version = "0.6.0" version = "0.6.0"
@@ -2923,8 +2971,7 @@ dependencies = [
[[package]] [[package]]
name = "lwk_wollet" name = "lwk_wollet"
version = "0.9.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/breez/lwk?rev=0b18e777d496#0b18e777d496bd3ee0602b49b838d3d293e43e23"
checksum = "44164918e75771585624098a328cf0d5e28931ccdd5af1c9a95a6ecf0c5cb67a"
dependencies = [ dependencies = [
"aes-gcm-siv", "aes-gcm-siv",
"age", "age",
@@ -3050,6 +3097,7 @@ version = "2.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567496f13503d6cae8c9f961f34536850275f396307d7a6b981eef1464032f53" checksum = "567496f13503d6cae8c9f961f34536850275f396307d7a6b981eef1464032f53"
dependencies = [ dependencies = [
"base64 0.12.3",
"log", "log",
"serde", "serde",
"serde_json", "serde_json",
@@ -3947,6 +3995,51 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper-rustls 0.24.2",
"hyper-tls 0.5.0",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"tokio",
"tokio-native-tls",
"tokio-rustls 0.24.1",
"tokio-socks",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.25.4",
"winreg",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.12" version = "0.12.12"
@@ -3962,8 +4055,8 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-rustls", "hyper-rustls 0.27.5",
"hyper-tls", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
@@ -3978,7 +4071,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tower 0.5.2", "tower 0.5.2",
@@ -4007,8 +4100,8 @@ dependencies = [
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-rustls", "hyper-rustls 0.27.5",
"hyper-tls", "hyper-tls 0.6.0",
"hyper-util", "hyper-util",
"ipnet", "ipnet",
"js-sys", "js-sys",
@@ -4026,7 +4119,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"system-configuration", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls 0.26.2", "tokio-rustls 0.26.2",
@@ -4209,6 +4302,18 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "rustls"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring 0.17.14",
"rustls-webpki 0.101.7",
"sct",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.25" version = "0.23.25"
@@ -4219,7 +4324,7 @@ dependencies = [
"once_cell", "once_cell",
"ring 0.17.14", "ring 0.17.14",
"rustls-pki-types", "rustls-pki-types",
"rustls-webpki", "rustls-webpki 0.103.0",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -4263,6 +4368,16 @@ dependencies = [
"web-time", "web-time",
] ]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring 0.17.14",
"untrusted 0.9.0",
]
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.0" version = "0.103.0"
@@ -4801,6 +4916,17 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys 0.5.0",
]
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.6.1" version = "0.6.1"
@@ -4809,7 +4935,17 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"core-foundation", "core-foundation",
"system-configuration-sys", "system-configuration-sys 0.6.0",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
] ]
[[package]] [[package]]
@@ -5034,6 +5170,16 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "tokio-rustls"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
dependencies = [
"rustls 0.21.12",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.2" version = "0.26.2"
@@ -5044,6 +5190,18 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror 1.0.69",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.17" version = "0.1.17"

View File

@@ -635,6 +635,25 @@ typedef struct wire_cst_sdk_event {
union SdkEventKind kind; union SdkEventKind kind;
} wire_cst_sdk_event; } wire_cst_sdk_event;
typedef struct wire_cst_BlockchainExplorer_Electrum {
struct wire_cst_list_prim_u_8_strict *url;
} wire_cst_BlockchainExplorer_Electrum;
typedef struct wire_cst_BlockchainExplorer_Esplora {
struct wire_cst_list_prim_u_8_strict *url;
bool use_waterfalls;
} wire_cst_BlockchainExplorer_Esplora;
typedef union BlockchainExplorerKind {
struct wire_cst_BlockchainExplorer_Electrum Electrum;
struct wire_cst_BlockchainExplorer_Esplora Esplora;
} BlockchainExplorerKind;
typedef struct wire_cst_blockchain_explorer {
int32_t tag;
union BlockchainExplorerKind kind;
} wire_cst_blockchain_explorer;
typedef struct wire_cst_external_input_parser { typedef struct wire_cst_external_input_parser {
struct wire_cst_list_prim_u_8_strict *provider_id; struct wire_cst_list_prim_u_8_strict *provider_id;
struct wire_cst_list_prim_u_8_strict *input_regex; struct wire_cst_list_prim_u_8_strict *input_regex;
@@ -659,9 +678,8 @@ typedef struct wire_cst_list_asset_metadata {
} wire_cst_list_asset_metadata; } wire_cst_list_asset_metadata;
typedef struct wire_cst_config { typedef struct wire_cst_config {
struct wire_cst_list_prim_u_8_strict *liquid_electrum_url; struct wire_cst_blockchain_explorer liquid_explorer;
struct wire_cst_list_prim_u_8_strict *bitcoin_electrum_url; struct wire_cst_blockchain_explorer bitcoin_explorer;
struct wire_cst_list_prim_u_8_strict *mempoolspace_url;
struct wire_cst_list_prim_u_8_strict *working_dir; struct wire_cst_list_prim_u_8_strict *working_dir;
struct wire_cst_list_prim_u_8_strict *cache_dir; struct wire_cst_list_prim_u_8_strict *cache_dir;
int32_t network; int32_t network;

View File

@@ -325,10 +325,15 @@ enum PaymentError {
"SignerError", "SignerError",
}; };
[Enum]
interface BlockchainExplorer {
Electrum(string url);
Esplora(string url, boolean use_waterfalls);
};
dictionary Config { dictionary Config {
string liquid_electrum_url; BlockchainExplorer liquid_explorer;
string bitcoin_electrum_url; BlockchainExplorer bitcoin_explorer;
string mempoolspace_url;
string working_dir; string working_dir;
LiquidNetwork network; LiquidNetwork network;
u64 payment_timeout_sec; u64 payment_timeout_sec;

View File

@@ -61,6 +61,7 @@ tempfile = "3"
ecies = { version = "0.2.7", default-features = false, features = ["pure"] } ecies = { version = "0.2.7", default-features = false, features = ["pure"] }
semver = "1.0.23" semver = "1.0.23"
lazy_static = "1.5.0" lazy_static = "1.5.0"
esplora-client = { git = "https://github.com/hydra-yse/rust-esplora-client", branch = "scripthash-utxo", features = ["async-https-rustls"] }
# Non-Wasm dependencies # Non-Wasm dependencies
[target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies]
@@ -68,7 +69,7 @@ electrum-client = { version = "0.21.0", default-features = false, features = [
"use-rustls-ring", "use-rustls-ring",
"proxy", "proxy",
] } ] }
lwk_wollet = { version = "0.9.0" } lwk_wollet = { git = "https://github.com/breez/lwk", rev = "0b18e777d496" }
maybe-sync = { version = "0.1.1", features = ["sync"] } maybe-sync = { version = "0.1.1", features = ["sync"] }
prost = "^0.11" prost = "^0.11"
tonic = { version = "^0.8", features = ["tls", "tls-webpki-roots"] } tonic = { version = "^0.8", features = ["tls", "tls-webpki-roots"] }
@@ -77,7 +78,7 @@ uuid = { version = "1.8.0", features = ["v4"] }
# Wasm dependencies # Wasm dependencies
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
console_log = "1" console_log = "1"
lwk_wollet = { version = "0.9.0", default-features = false, features = [ lwk_wollet = { git = "https://github.com/breez/lwk", rev = "0b18e777d496", default-features = false, features = [
"esplora", "esplora",
] } ] }
maybe-sync = "0.1.1" maybe-sync = "0.1.1"

View File

@@ -1,114 +1,54 @@
use std::{ #![cfg(not(all(target_family = "wasm", target_os = "unknown")))]
collections::HashMap,
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
use anyhow::{anyhow, Result}; use std::{collections::HashMap, sync::OnceLock, time::Duration};
use electrum_client::{
use anyhow::{anyhow, bail, Result};
use tokio::sync::Mutex;
use crate::{
bitcoin::{ bitcoin::{
consensus::{deserialize, serialize}, consensus::{deserialize, serialize},
hashes::{sha256, Hash}, hashes::{sha256, Hash},
Address, OutPoint, Script, Transaction, Txid, Address, OutPoint, Script, ScriptBuf, Transaction, Txid,
}, },
Client, ElectrumApi, GetBalanceRes, HeaderNotification, model::{BlockchainExplorer, Config, LiquidNetwork, RecommendedFees, Utxo},
}; };
use electrum_client::{Client, ElectrumApi, HeaderNotification};
use log::info; use log::info;
use lwk_wollet::{bitcoin::ScriptBuf, ElectrumOptions, ElectrumUrl, Error, History}; use lwk_wollet::{ElectrumOptions, ElectrumUrl};
use sdk_common::{ use sdk_common::bitcoin::hashes::hex::ToHex as _;
bitcoin::hashes::hex::ToHex,
prelude::{get_and_check_success, parse_json, RestClient},
};
use crate::{ use super::{BitcoinChainService, BtcScriptBalance, History};
model::{Config, LiquidNetwork, RecommendedFees},
prelude::Utxo,
};
/// Trait implemented by types that can fetch data from a blockchain data source. pub(crate) struct ElectrumBitcoinChainService {
#[allow(dead_code)]
#[sdk_macros::async_trait]
pub trait BitcoinChainService: Send + Sync {
/// Get the blockchain latest block
fn tip(&self) -> Result<HeaderNotification>;
/// Broadcast a transaction
fn broadcast(&self, tx: &Transaction) -> Result<Txid>;
/// Get a list of transactions
fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>>;
/// Get the transactions involved for a script
fn get_script_history(&self, script: &Script) -> Result<Vec<History>>;
/// Get the transactions involved in a list of scripts.
fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>>;
/// Get the transactions involved for a script
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>>;
/// Get the utxos associated with a script pubkey
fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>>;
/// Get the utxos associated with a list of scripts
fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>>;
/// Return the confirmed and unconfirmed balances of a script hash
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes>;
/// Return the confirmed and unconfirmed balances of a list of script hashes
fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<GetBalanceRes>>;
/// Return the confirmed and unconfirmed balances of a script hash
async fn script_get_balance_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<GetBalanceRes>;
/// Verify that a transaction appears in the address script history
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction>;
/// Get the recommended fees, in sat/vbyte
async fn recommended_fees(&self) -> Result<RecommendedFees>;
}
pub(crate) struct HybridBitcoinChainService {
config: Config, config: Config,
rest_client: Arc<dyn RestClient>,
client: OnceLock<Client>, client: OnceLock<Client>,
last_known_tip: Mutex<Option<HeaderNotification>>, last_known_tip: Mutex<Option<u32>>,
} }
impl HybridBitcoinChainService {
pub fn new(config: Config, rest_client: Arc<dyn RestClient>) -> Result<Self, Error> { impl ElectrumBitcoinChainService {
Ok(Self { pub(crate) fn new(config: Config) -> Self {
Self {
config, config,
rest_client,
client: OnceLock::new(), client: OnceLock::new(),
last_known_tip: Mutex::new(None), last_known_tip: Mutex::new(None),
}) }
} }
fn get_client(&self) -> Result<&Client> { fn get_client(&self) -> Result<&Client> {
if let Some(c) = self.client.get() { if let Some(c) = self.client.get() {
return Ok(c); return Ok(c);
} }
let (tls, validate_domain) = match self.config.network { let (tls, validate_domain) = match self.config.network {
LiquidNetwork::Mainnet | LiquidNetwork::Testnet => (true, true), LiquidNetwork::Mainnet | LiquidNetwork::Testnet => (true, true),
LiquidNetwork::Regtest => (false, false), LiquidNetwork::Regtest => (false, false),
}; };
let electrum_url = let electrum_url = match &self.config.bitcoin_explorer {
ElectrumUrl::new(&self.config.bitcoin_electrum_url, tls, validate_domain)?; BlockchainExplorer::Electrum { url } => ElectrumUrl::new(url, tls, validate_domain)?,
_ => bail!("Cannot start Bitcoin Electrum chain service without an Electrum url"),
};
let client = electrum_url.build_client(&ElectrumOptions { timeout: Some(3) })?; let client = electrum_url.build_client(&ElectrumOptions { timeout: Some(3) })?;
let client = self.client.get_or_init(|| client); let client = self.client.get_or_init(|| client);
@@ -117,8 +57,8 @@ impl HybridBitcoinChainService {
} }
#[sdk_macros::async_trait] #[sdk_macros::async_trait]
impl BitcoinChainService for HybridBitcoinChainService { impl BitcoinChainService for ElectrumBitcoinChainService {
fn tip(&self) -> Result<HeaderNotification> { async fn tip(&self) -> Result<u32> {
let client = self.get_client()?; let client = self.get_client()?;
let mut maybe_popped_header = None; let mut maybe_popped_header = None;
while let Some(header) = client.block_headers_pop_raw()? { while let Some(header) = client.block_headers_pop_raw()? {
@@ -140,24 +80,25 @@ impl BitcoinChainService for HybridBitcoinChainService {
} }
}; };
let mut last_tip = self.last_known_tip.lock().unwrap(); let mut last_tip = self.last_known_tip.lock().await;
match new_tip { match new_tip {
Some(header) => { Some(header) => {
*last_tip = Some(header.clone()); let height = header.height as u32;
Ok(header) *last_tip = Some(height);
Ok(height)
} }
None => last_tip.clone().ok_or_else(|| anyhow!("Failed to get tip")), None => (*last_tip).ok_or_else(|| anyhow!("Failed to get tip")),
} }
} }
fn broadcast(&self, tx: &Transaction) -> Result<Txid> { async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
let txid = self let txid = self
.get_client()? .get_client()?
.transaction_broadcast_raw(&serialize(&tx))?; .transaction_broadcast_raw(&serialize(&tx))?;
Ok(Txid::from_raw_hash(txid.to_raw_hash())) Ok(Txid::from_raw_hash(txid.to_raw_hash()))
} }
fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> { async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
let mut result = vec![]; let mut result = vec![];
for tx in self.get_client()?.batch_transaction_get_raw(txids)? { for tx in self.get_client()?.batch_transaction_get_raw(txids)? {
let tx: Transaction = deserialize(&tx)?; let tx: Transaction = deserialize(&tx)?;
@@ -166,7 +107,7 @@ impl BitcoinChainService for HybridBitcoinChainService {
Ok(result) Ok(result)
} }
fn get_script_history(&self, script: &Script) -> Result<Vec<History>> { async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
Ok(self Ok(self
.get_client()? .get_client()?
.script_get_history(script)? .script_get_history(script)?
@@ -175,7 +116,7 @@ impl BitcoinChainService for HybridBitcoinChainService {
.collect()) .collect())
} }
fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>> { async fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>> {
Ok(self Ok(self
.get_client()? .get_client()?
.batch_script_get_history(scripts)? .batch_script_get_history(scripts)?
@@ -195,7 +136,7 @@ impl BitcoinChainService for HybridBitcoinChainService {
let mut retry = 0; let mut retry = 0;
while retry <= retries { while retry <= retries {
script_history = self.get_script_history(script)?; script_history = self.get_script_history(script).await?;
match script_history.is_empty() { match script_history.is_empty() {
true => { true => {
retry += 1; retry += 1;
@@ -211,22 +152,25 @@ impl BitcoinChainService for HybridBitcoinChainService {
Ok(script_history) Ok(script_history)
} }
fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> { async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
Ok(self Ok(self
.get_scripts_utxos(&[script])? .get_scripts_utxos(&[script])
.await?
.first() .first()
.cloned() .cloned()
.unwrap_or_default()) .unwrap_or_default())
} }
fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>> { async fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>> {
let scripts_history = self.get_scripts_history(scripts)?; let scripts_history = self.get_scripts_history(scripts).await?;
let tx_confirmed_map: HashMap<_, _> = scripts_history let tx_confirmed_map: HashMap<_, _> = scripts_history
.iter() .iter()
.flatten() .flatten()
.map(|h| (Txid::from_raw_hash(h.txid.to_raw_hash()), h.height > 0)) .map(|h| (Txid::from_raw_hash(h.txid.to_raw_hash()), h.height > 0))
.collect(); .collect();
let txs = self.get_transactions(&tx_confirmed_map.keys().cloned().collect::<Vec<_>>())?; let txs = self
.get_transactions(&tx_confirmed_map.keys().cloned().collect::<Vec<_>>())
.await?;
let script_txs_map: HashMap<ScriptBuf, Vec<Transaction>> = scripts let script_txs_map: HashMap<ScriptBuf, Vec<Transaction>> = scripts
.iter() .iter()
.map(|script| ScriptBuf::from_bytes(script.to_bytes().to_vec())) .map(|script| ScriptBuf::from_bytes(script.to_bytes().to_vec()))
@@ -287,31 +231,36 @@ impl BitcoinChainService for HybridBitcoinChainService {
Ok(scripts_utxos) Ok(scripts_utxos)
} }
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes> { async fn script_get_balance(&self, script: &Script) -> Result<BtcScriptBalance> {
Ok(self.get_client()?.script_get_balance(script)?) Ok(self.get_client()?.script_get_balance(script)?.into())
} }
fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<GetBalanceRes>> { async fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<BtcScriptBalance>> {
Ok(self.get_client()?.batch_script_get_balance(scripts)?) Ok(self
.get_client()?
.batch_script_get_balance(scripts)?
.into_iter()
.map(Into::into)
.collect())
} }
async fn script_get_balance_with_retry( async fn script_get_balance_with_retry(
&self, &self,
script: &Script, script: &Script,
retries: u64, retries: u64,
) -> Result<GetBalanceRes> { ) -> Result<BtcScriptBalance> {
let script_hash = sha256::Hash::hash(script.as_bytes()).to_hex(); let script_hash = sha256::Hash::hash(script.as_bytes()).to_hex();
info!("Fetching script balance for {}", script_hash); info!("Fetching script balance for {}", script_hash);
let mut script_balance = GetBalanceRes { let mut script_balance = BtcScriptBalance {
confirmed: 0, confirmed: 0,
unconfirmed: 0, unconfirmed: 0,
}; };
let mut retry = 0; let mut retry = 0;
while retry <= retries { while retry <= retries {
script_balance = self.script_get_balance(script)?; script_balance = self.script_get_balance(script).await?;
match script_balance { match script_balance {
GetBalanceRes { BtcScriptBalance {
confirmed: 0, confirmed: 0,
unconfirmed: 0, unconfirmed: 0,
} => { } => {
@@ -368,11 +317,18 @@ impl BitcoinChainService for HybridBitcoinChainService {
} }
async fn recommended_fees(&self) -> Result<RecommendedFees> { async fn recommended_fees(&self) -> Result<RecommendedFees> {
let (response, _) = get_and_check_success( let fees: Vec<u64> = self
self.rest_client.as_ref(), .get_client()?
&format!("{}/v1/fees/recommended", self.config.mempoolspace_url), .batch_estimate_fee([1, 3, 6, 25, 1008])?
) .into_iter()
.await?; .map(|v| v.ceil() as u64)
Ok(parse_json(&response)?) .collect();
Ok(RecommendedFees {
fastest_fee: fees[0],
half_hour_fee: fees[1],
hour_fee: fees[2],
economy_fee: fees[3],
minimum_fee: fees[4],
})
} }
} }

View File

@@ -0,0 +1,333 @@
use std::{collections::HashMap, sync::OnceLock, time::Duration};
use esplora_client::{AsyncClient, Builder};
use tokio::sync::Mutex;
use crate::{
bitcoin::{
consensus::deserialize,
hashes::{sha256, Hash},
Address, OutPoint, Script, ScriptBuf, Transaction, Txid,
},
model::{BlockchainExplorer, Config},
};
use anyhow::{anyhow, bail, Context, Result};
use crate::model::{RecommendedFees, Utxo};
use log::info;
use sdk_common::bitcoin::hashes::hex::ToHex as _;
use super::{BitcoinChainService, BtcScriptBalance, History};
pub(crate) struct EsploraBitcoinChainService {
config: Config,
client: OnceLock<AsyncClient>,
last_known_tip: Mutex<Option<u32>>,
}
impl EsploraBitcoinChainService {
pub(crate) fn new(config: Config) -> Self {
Self {
config,
client: OnceLock::new(),
last_known_tip: Mutex::new(None),
}
}
fn get_client(&self) -> Result<&AsyncClient> {
if let Some(c) = self.client.get() {
return Ok(c);
}
let esplora_url = match &self.config.bitcoin_explorer {
BlockchainExplorer::Esplora { url, .. } => url,
_ => bail!("Cannot start Bitcoin Esplora chain service without an Esplora url"),
};
let client = Builder::new(esplora_url)
.timeout(3)
.max_retries(10)
.build_async()?;
let client = self.client.get_or_init(|| client);
Ok(client)
}
}
#[sdk_macros::async_trait]
impl BitcoinChainService for EsploraBitcoinChainService {
async fn tip(&self) -> Result<u32> {
let client = self.get_client()?;
let new_tip = client.get_height().await.ok();
let mut last_tip = self.last_known_tip.lock().await;
match new_tip {
Some(height) => {
*last_tip = Some(height);
Ok(height)
}
None => (*last_tip).ok_or_else(|| anyhow!("Failed to get tip")),
}
}
async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
self.get_client()?.broadcast(tx).await?;
Ok(tx.compute_txid())
}
// TODO Switch to batch search
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
let client = self.get_client()?;
let mut result = vec![];
for txid in txids {
let tx = client
.get_tx(txid)
.await?
.context("Transaction not found")?;
result.push(tx);
}
Ok(result)
}
async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
let client = self.get_client()?;
let history = client
.scripthash_txs(script, None)
.await?
.into_iter()
.map(|tx| History {
txid: tx.txid,
height: tx.status.block_height.map(|h| h as i32).unwrap_or(-1),
})
.collect();
Ok(history)
}
// TODO Switch to batch search
async fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>> {
let mut result = vec![];
for script in scripts {
let history = self.get_script_history(script).await?;
result.push(history);
}
Ok(result)
}
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>> {
let script_hash = sha256::Hash::hash(script.as_bytes()).to_hex();
info!("Fetching script history for {}", script_hash);
let mut script_history = vec![];
let mut retry = 0;
while retry <= retries {
script_history = self.get_script_history(script).await?;
match script_history.is_empty() {
true => {
retry += 1;
info!(
"Script history for {} got zero transactions, retrying in {} seconds...",
script_hash, retry
);
tokio::time::sleep(Duration::from_secs(retry)).await;
}
false => break,
}
}
Ok(script_history)
}
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
Ok(self
.get_scripts_utxos(&[script])
.await?
.first()
.cloned()
.unwrap_or_default())
}
async fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>> {
let scripts_history = self.get_scripts_history(scripts).await?;
let tx_confirmed_map: HashMap<_, _> = scripts_history
.iter()
.flatten()
.map(|h| (h.txid, h.height > 0))
.collect();
let txs = self
.get_transactions(&tx_confirmed_map.keys().cloned().collect::<Vec<_>>())
.await?;
let script_txs_map: HashMap<ScriptBuf, Vec<Transaction>> = scripts
.iter()
.map(|script| ScriptBuf::from_bytes(script.to_bytes().to_vec()))
.zip(scripts_history)
.map(|(script_buf, script_history)| {
(
script_buf,
script_history
.iter()
.filter_map(|h| {
txs.iter()
.find(|tx| tx.compute_txid().as_raw_hash() == h.txid.as_raw_hash())
.cloned()
})
.collect::<Vec<_>>(),
)
})
.collect();
let scripts_utxos = script_txs_map
.iter()
.map(|(script_buf, txs)| {
txs.iter()
.flat_map(|tx| {
tx.output
.iter()
.enumerate()
.filter(|(_, output)| output.script_pubkey == *script_buf)
.filter(|(vout, _)| {
// Check if output is unspent (only consider confirmed spending txs)
!txs.iter().any(|spending_tx| {
let spends_our_output = spending_tx.input.iter().any(|input| {
input.previous_output.txid == tx.compute_txid()
&& input.previous_output.vout == *vout as u32
});
if spends_our_output {
// If it does spend our output, check if it's confirmed
let spending_tx_hash = spending_tx.compute_txid();
tx_confirmed_map
.get(&spending_tx_hash)
.copied()
.unwrap_or(false)
} else {
false
}
})
})
.map(|(vout, output)| {
Utxo::Bitcoin((
OutPoint::new(tx.compute_txid(), vout as u32),
output.clone(),
))
})
})
.collect()
})
.collect();
Ok(scripts_utxos)
}
async fn script_get_balance(&self, script: &Script) -> Result<BtcScriptBalance> {
let client = self.get_client()?;
let utxos = client.scripthash_utxos(script).await?;
let mut balance = BtcScriptBalance {
confirmed: 0,
unconfirmed: 0,
};
for utxo in utxos {
match utxo.status.confirmed {
true => balance.confirmed += utxo.value,
false => balance.unconfirmed += utxo.value as i64,
};
}
Ok(balance)
}
// TODO Switch to batch search
async fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<BtcScriptBalance>> {
let mut result = vec![];
for script in scripts {
let balance = self.script_get_balance(script).await?;
result.push(balance);
}
Ok(result)
}
async fn script_get_balance_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<BtcScriptBalance> {
let script_hash = sha256::Hash::hash(script.as_bytes()).to_hex();
info!("Fetching script balance for {}", script_hash);
let mut script_balance = BtcScriptBalance {
confirmed: 0,
unconfirmed: 0,
};
let mut retry = 0;
while retry <= retries {
script_balance = self.script_get_balance(script).await?;
match script_balance {
BtcScriptBalance {
confirmed: 0,
unconfirmed: 0,
} => {
retry += 1;
info!(
"Got zero balance for script {}, retrying in {} seconds...",
script_hash, retry
);
tokio::time::sleep(Duration::from_secs(retry)).await;
}
_ => break,
}
}
Ok(script_balance)
}
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction> {
let script = address.script_pubkey();
let script_history = self.get_script_history_with_retry(&script, 10).await?;
let lockup_tx_history = script_history.iter().find(|h| h.txid.to_hex().eq(tx_id));
match lockup_tx_history {
Some(history) => {
info!("Bitcoin transaction found, verifying transaction content...");
let tx: Transaction = deserialize(&hex::decode(tx_hex)?)?;
let tx_hex = tx.compute_txid().to_hex();
if !tx_hex.eq(&history.txid.to_hex()) {
return Err(anyhow!(
"Bitcoin transaction id and hex do not match: {} vs {}",
tx_id,
tx_hex
));
}
if verify_confirmation && history.height <= 0 {
return Err(anyhow!(
"Bitcoin transaction was not confirmed, txid={} waiting for confirmation",
tx_id,
));
}
Ok(tx)
}
None => Err(anyhow!(
"Bitcoin transaction was not found, txid={} waiting for broadcast",
tx_id,
)),
}
}
async fn recommended_fees(&self) -> Result<RecommendedFees> {
let client = self.get_client()?;
let fees = client.get_fee_estimates().await?;
let get_fees = |block: &u16| fees.get(block).map(|fee| fee.ceil() as u64).unwrap_or(0);
Ok(RecommendedFees {
fastest_fee: get_fees(&1),
half_hour_fee: get_fees(&3),
hour_fee: get_fees(&6),
economy_fee: get_fees(&25),
minimum_fee: get_fees(&1008),
})
}
}

View File

@@ -0,0 +1,69 @@
pub(crate) mod electrum;
pub(crate) mod esplora;
use anyhow::Result;
use crate::{
bitcoin,
model::{BtcHistory, BtcScriptBalance, RecommendedFees, Utxo},
};
use bitcoin::{Address, Script, Transaction, Txid};
pub(crate) type History = BtcHistory;
/// Trait implemented by types that can fetch data from a blockchain data source.
#[sdk_macros::async_trait]
pub trait BitcoinChainService: Send + Sync {
/// Get the blockchain latest block
async fn tip(&self) -> Result<u32>;
/// Broadcast a transaction
async fn broadcast(&self, tx: &Transaction) -> Result<Txid>;
/// Get a list of transactions
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>>;
/// Get the transactions involved for a script
async fn get_script_history(&self, script: &Script) -> Result<Vec<History>>;
/// Get the transactions involved in a list of scripts.
async fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>>;
/// Get the transactions involved for a script
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>>;
/// Get the utxos associated with a script pubkey
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>>;
/// Get the utxos associated with a list of scripts
async fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>>;
/// Return the confirmed and unconfirmed balances of a script hash
async fn script_get_balance(&self, script: &Script) -> Result<BtcScriptBalance>;
/// Return the confirmed and unconfirmed balances of a list of script hashes
async fn scripts_get_balance(&self, scripts: &[&Script]) -> Result<Vec<BtcScriptBalance>>;
/// Return the confirmed and unconfirmed balances of a script hash
async fn script_get_balance_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<BtcScriptBalance>;
/// Verify that a transaction appears in the address script history
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction>;
/// Get the recommended fees, in sat/vbyte
async fn recommended_fees(&self) -> Result<RecommendedFees>;
}

View File

@@ -1,286 +0,0 @@
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use anyhow::{anyhow, Result};
use boltz_client::ToHex;
use electrum_client::{Client, ElectrumApi};
use elements::encode::serialize as elements_serialize;
use log::info;
use lwk_wollet::elements::hex::FromHex;
use lwk_wollet::{bitcoin, elements, ElectrumOptions};
use lwk_wollet::{
elements::{Address, OutPoint, Script, Transaction, Txid},
hashes::{sha256, Hash},
ElectrumUrl, History,
};
use mockall::automock;
use crate::model::LiquidNetwork;
use crate::prelude::Utxo;
use crate::{model::Config, utils};
#[automock]
#[sdk_macros::async_trait]
pub trait LiquidChainService: Send + Sync {
/// Get the blockchain latest block
async fn tip(&self) -> Result<u32>;
/// Broadcast a transaction
async fn broadcast(&self, tx: &Transaction) -> Result<Txid>;
/// Get a single transaction from its raw hash
async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>>;
/// Get a list of transactions
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>>;
/// Get the transactions involved in a script
async fn get_script_history(&self, scripts: &Script) -> Result<Vec<History>>;
/// Get the transactions involved in a list of scripts.
///
/// The data is fetched in a single call from the Electrum endpoint.
async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>>;
/// Get the transactions involved in a list of scripts
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>>;
/// Get the utxos associated with a script pubkey
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>>;
/// Verify that a transaction appears in the address script history
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction>;
}
pub(crate) struct HybridLiquidChainService {
client: OnceLock<Client>,
config: Config,
last_known_tip: Mutex<Option<u32>>,
}
impl HybridLiquidChainService {
pub(crate) fn new(config: Config) -> Result<Self> {
Ok(Self {
config,
client: OnceLock::new(),
last_known_tip: Mutex::new(None),
})
}
fn get_client(&self) -> Result<&Client> {
if let Some(c) = self.client.get() {
return Ok(c);
}
let (tls, validate_domain) = match self.config.network {
LiquidNetwork::Mainnet | LiquidNetwork::Testnet => (true, true),
LiquidNetwork::Regtest => (false, false),
};
let electrum_url =
ElectrumUrl::new(&self.config.liquid_electrum_url, tls, validate_domain)?;
let client = electrum_url.build_client(&ElectrumOptions { timeout: Some(3) })?;
let client = self.client.get_or_init(|| client);
Ok(client)
}
}
#[sdk_macros::async_trait]
impl LiquidChainService for HybridLiquidChainService {
async fn tip(&self) -> Result<u32> {
let client = self.get_client()?;
let mut maybe_popped_header = None;
while let Some(header) = client.block_headers_pop_raw()? {
maybe_popped_header = Some(header)
}
let new_tip: Option<u32> = match maybe_popped_header {
Some(popped_header) => Some(popped_header.height.try_into()?),
None => {
// https://github.com/bitcoindevkit/rust-electrum-client/issues/124
// It might be that the client has reconnected and subscriptions don't persist
// across connections. Calling `client.ping()` won't help here because the
// successful retry will prevent us knowing about the reconnect.
if let Ok(header) = client.block_headers_subscribe_raw() {
Some(header.height.try_into()?)
} else {
None
}
}
};
let mut last_tip: std::sync::MutexGuard<'_, Option<u32>> =
self.last_known_tip.lock().unwrap();
match new_tip {
Some(height) => {
*last_tip = Some(height);
Ok(height)
}
None => last_tip.ok_or_else(|| anyhow!("Failed to get tip")),
}
}
async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
let txid = self
.get_client()?
.transaction_broadcast_raw(&elements_serialize(tx))?;
Ok(Txid::from_raw_hash(txid.to_raw_hash()))
}
async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>> {
Ok(self.get_transactions(&[*txid]).await?.first().cloned())
}
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
let txids: Vec<bitcoin::Txid> = txids
.iter()
.map(|t| bitcoin::Txid::from_raw_hash(t.to_raw_hash()))
.collect();
let mut result = vec![];
for tx in self.get_client()?.batch_transaction_get_raw(&txids)? {
let tx: Transaction = elements::encode::deserialize(&tx)?;
result.push(tx);
}
Ok(result)
}
async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
let scripts = &[script];
let scripts: Vec<&bitcoin::Script> = scripts
.iter()
.map(|t| bitcoin::Script::from_bytes(t.as_bytes()))
.collect();
let mut history_vec: Vec<Vec<History>> = self
.get_client()?
.batch_script_get_history(&scripts)?
.into_iter()
.map(|e| e.into_iter().map(Into::into).collect())
.collect();
let h = history_vec.pop();
Ok(h.unwrap_or_default())
}
async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>> {
let scripts: Vec<&bitcoin::Script> = scripts
.iter()
.map(|t| bitcoin::Script::from_bytes(t.as_bytes()))
.collect();
Ok(self
.get_client()?
.batch_script_get_history(&scripts)?
.into_iter()
.map(|e| e.into_iter().map(Into::into).collect())
.collect())
}
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>> {
let script_hash = sha256::Hash::hash(script.as_bytes())
.to_byte_array()
.to_hex();
info!("Fetching script history for {}", script_hash);
let mut script_history = vec![];
let mut retry = 0;
while retry <= retries {
script_history = self.get_script_history(script).await?;
match script_history.is_empty() {
true => {
retry += 1;
info!("Script history for {script_hash} is empty, retrying in 1 second... ({retry} of {retries})");
// Waiting 1s between retries, so we detect the new tx as soon as possible
tokio::time::sleep(Duration::from_secs(1)).await;
}
false => break,
}
}
Ok(script_history)
}
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
let history = self.get_script_history_with_retry(script, 10).await?;
let mut utxos: Vec<Utxo> = vec![];
for history_item in history {
match self.get_transaction_hex(&history_item.txid).await {
Ok(Some(tx)) => {
let mut new_utxos = tx
.output
.iter()
.enumerate()
.map(|(vout, output)| {
Utxo::Liquid(Box::new((
OutPoint::new(history_item.txid, vout as u32),
output.clone(),
)))
})
.collect();
utxos.append(&mut new_utxos);
}
_ => {
log::warn!("Could not retrieve transaction from history item");
continue;
}
}
}
return Ok(utxos);
}
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction> {
let script = Script::from_hex(
hex::encode(address.to_unconfidential().script_pubkey().as_bytes()).as_str(),
)
.map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
let script_history = self.get_script_history_with_retry(&script, 30).await?;
let lockup_tx_history = script_history.iter().find(|h| h.txid.to_hex().eq(tx_id));
match lockup_tx_history {
Some(history) => {
info!("Liquid transaction found, verifying transaction content...");
let tx: Transaction = utils::deserialize_tx_hex(tx_hex)?;
if !tx.txid().to_hex().eq(&history.txid.to_hex()) {
return Err(anyhow!(
"Liquid transaction id and hex do not match: {} vs {}",
tx_id,
tx.txid().to_hex()
));
}
if verify_confirmation && history.height <= 0 {
return Err(anyhow!(
"Liquid transaction was not confirmed, txid={} waiting for confirmation",
tx_id,
));
}
Ok(tx)
}
None => Err(anyhow!(
"Liquid transaction was not found, txid={} waiting for broadcast",
tx_id,
)),
}
}
}

View File

@@ -0,0 +1,195 @@
#![cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use std::{sync::OnceLock, time::Duration};
use anyhow::{anyhow, bail, Context as _, Result};
use tokio::sync::RwLock;
use crate::{
elements::{Address, OutPoint, Script, Transaction, Txid},
model::{BlockchainExplorer, Config, LiquidNetwork, Utxo},
utils,
};
use log::info;
use lwk_wollet::{
clients::blocking::BlockchainBackend as _, elements::hex::FromHex as _, ElectrumClient,
ElectrumOptions, ElectrumUrl,
};
use sdk_common::bitcoin::hashes::hex::ToHex as _;
use super::{History, LiquidChainService};
pub(crate) struct ElectrumLiquidChainService {
config: Config,
client: OnceLock<RwLock<ElectrumClient>>,
}
impl ElectrumLiquidChainService {
pub(crate) fn new(config: Config) -> Self {
Self {
config,
client: OnceLock::new(),
}
}
fn get_client(&self) -> Result<&RwLock<ElectrumClient>> {
if let Some(c) = self.client.get() {
return Ok(c);
}
let (tls, validate_domain) = match self.config.network {
LiquidNetwork::Mainnet | LiquidNetwork::Testnet => (true, true),
LiquidNetwork::Regtest => (false, false),
};
let electrum_url = match &self.config.liquid_explorer {
BlockchainExplorer::Electrum { url } => ElectrumUrl::new(url, tls, validate_domain)?,
_ => bail!("Cannot start Liquid Electrum chain service without an Electrum url"),
};
let client =
ElectrumClient::with_options(&electrum_url, ElectrumOptions { timeout: Some(3) })?;
let client = self.client.get_or_init(|| RwLock::new(client));
Ok(client)
}
}
#[sdk_macros::async_trait]
impl LiquidChainService for ElectrumLiquidChainService {
async fn tip(&self) -> Result<u32> {
Ok(self
.get_client()?
.write()
.await
.tip()
.map(|header| header.height)?)
}
async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
Ok(self.get_client()?.read().await.broadcast(tx)?)
}
async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>> {
Ok(self.get_transactions(&[*txid]).await?.first().cloned())
}
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
Ok(self.get_client()?.read().await.get_transactions(txids)?)
}
async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
self.get_scripts_history(&[script.clone()])
.await?
.into_iter()
.nth(0)
.context("History not found")
}
async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>> {
let scripts: Vec<&Script> = scripts.iter().collect();
Ok(self
.get_client()?
.read()
.await
.get_scripts_history(&scripts)?
.into_iter()
.map(|h| h.into_iter().map(Into::into).collect())
.collect())
}
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>> {
info!("Fetching script history for {script:x}");
let mut script_history = vec![];
let mut retry = 0;
while retry <= retries {
script_history = self.get_script_history(script).await?;
match script_history.is_empty() {
true => {
retry += 1;
info!("Script history for {script:x} is empty, retrying in 1 second... ({retry} of {retries})");
// Waiting 1s between retries, so we detect the new tx as soon as possible
tokio::time::sleep(Duration::from_secs(1)).await;
}
false => break,
}
}
Ok(script_history)
}
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
let history = self.get_script_history_with_retry(script, 10).await?;
let mut utxos: Vec<Utxo> = vec![];
for history_item in history {
match self.get_transaction_hex(&history_item.txid).await {
Ok(Some(tx)) => {
let mut new_utxos = tx
.output
.iter()
.enumerate()
.map(|(vout, output)| {
Utxo::Liquid(Box::new((
OutPoint::new(history_item.txid, vout as u32),
output.clone(),
)))
})
.collect();
utxos.append(&mut new_utxos);
}
_ => {
log::warn!("Could not retrieve transaction from history item");
continue;
}
}
}
Ok(utxos)
}
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction> {
let script = Script::from_hex(
hex::encode(address.to_unconfidential().script_pubkey().as_bytes()).as_str(),
)
.map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
let script_history = self.get_script_history_with_retry(&script, 30).await?;
let lockup_tx_history = script_history.iter().find(|h| h.txid.to_hex().eq(tx_id));
match lockup_tx_history {
Some(history) => {
info!("Liquid transaction found, verifying transaction content...");
let tx: Transaction = utils::deserialize_tx_hex(tx_hex)?;
if !tx.txid().to_hex().eq(&history.txid.to_hex()) {
return Err(anyhow!(
"Liquid transaction id and hex do not match: {} vs {}",
tx_id,
tx.txid().to_hex()
));
}
if verify_confirmation && history.height <= 0 {
return Err(anyhow!(
"Liquid transaction was not confirmed, txid={} waiting for confirmation",
tx_id,
));
}
Ok(tx)
}
None => Err(anyhow!(
"Liquid transaction was not found, txid={} waiting for broadcast",
tx_id,
)),
}
}
}

View File

@@ -0,0 +1,199 @@
use std::{sync::OnceLock, time::Duration};
use anyhow::{anyhow, bail, Context as _, Result};
use tokio::sync::RwLock;
use crate::{
elements::{Address, OutPoint, Script, Transaction, Txid},
model::{BlockchainExplorer, Config, Utxo},
utils,
};
use log::info;
use lwk_wollet::{
asyncr::EsploraClientBuilder, clients::asyncr::EsploraClient, elements::hex::FromHex as _,
};
use sdk_common::bitcoin::hashes::hex::ToHex as _;
use super::{History, LiquidChainService};
pub(crate) struct EsploraLiquidChainService {
config: Config,
client: OnceLock<RwLock<EsploraClient>>,
}
impl EsploraLiquidChainService {
pub(crate) fn new(config: Config) -> Self {
Self {
config,
client: OnceLock::new(),
}
}
fn get_client(&self) -> Result<&RwLock<EsploraClient>> {
if let Some(c) = self.client.get() {
return Ok(c);
}
let client = match &self.config.liquid_explorer {
BlockchainExplorer::Esplora {
url,
use_waterfalls,
} => EsploraClientBuilder::new(url, self.config.network.into())
.timeout(3)
.waterfalls(*use_waterfalls)
.build(),
_ => bail!("Cannot start Liquid Esplroa chain service without an Esplora url"),
};
let client = self.client.get_or_init(|| RwLock::new(client));
Ok(client)
}
}
#[sdk_macros::async_trait]
impl LiquidChainService for EsploraLiquidChainService {
async fn tip(&self) -> Result<u32> {
Ok(self
.get_client()?
.write()
.await
.tip()
.await
.map(|header| header.height)?)
}
async fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
Ok(self.get_client()?.read().await.broadcast(tx).await?)
}
async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>> {
Ok(self.get_transactions(&[*txid]).await?.first().cloned())
}
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
Ok(self
.get_client()?
.read()
.await
.get_transactions(txids)
.await?)
}
async fn get_script_history(&self, script: &Script) -> Result<Vec<History>> {
self.get_scripts_history(&[script.clone()])
.await?
.into_iter()
.nth(0)
.context("History not found")
}
async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>> {
let scripts: Vec<&Script> = scripts.iter().collect();
Ok(self
.get_client()?
.read()
.await
.get_scripts_history(&scripts)
.await?
.into_iter()
.map(|h| h.into_iter().map(Into::into).collect())
.collect())
}
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>> {
info!("Fetching script history for {script:x}");
let mut script_history = vec![];
let mut retry = 0;
while retry <= retries {
script_history = self.get_script_history(script).await?;
match script_history.is_empty() {
true => {
retry += 1;
info!("Script history for {script:x} is empty, retrying in 1 second... ({retry} of {retries})");
// Waiting 1s between retries, so we detect the new tx as soon as possible
tokio::time::sleep(Duration::from_secs(1)).await;
}
false => break,
}
}
Ok(script_history)
}
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
let history = self.get_script_history_with_retry(script, 10).await?;
let mut utxos: Vec<Utxo> = vec![];
for history_item in history {
match self.get_transaction_hex(&history_item.txid).await {
Ok(Some(tx)) => {
let mut new_utxos = tx
.output
.iter()
.enumerate()
.map(|(vout, output)| {
Utxo::Liquid(Box::new((
OutPoint::new(history_item.txid, vout as u32),
output.clone(),
)))
})
.collect();
utxos.append(&mut new_utxos);
}
_ => {
log::warn!("Could not retrieve transaction from history item");
continue;
}
}
}
return Ok(utxos);
}
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction> {
let script = Script::from_hex(
hex::encode(address.to_unconfidential().script_pubkey().as_bytes()).as_str(),
)
.map_err(|e| anyhow!("Failed to get script from address {e:?}"))?;
let script_history = self.get_script_history_with_retry(&script, 30).await?;
let lockup_tx_history = script_history.iter().find(|h| h.txid.to_hex().eq(tx_id));
match lockup_tx_history {
Some(history) => {
info!("Liquid transaction found, verifying transaction content...");
let tx: Transaction = utils::deserialize_tx_hex(tx_hex)?;
if !tx.txid().to_hex().eq(&history.txid.to_hex()) {
return Err(anyhow!(
"Liquid transaction id and hex do not match: {} vs {}",
tx_id,
tx.txid().to_hex()
));
}
if verify_confirmation && history.height <= 0 {
return Err(anyhow!(
"Liquid transaction was not confirmed, txid={} waiting for confirmation",
tx_id,
));
}
Ok(tx)
}
None => Err(anyhow!(
"Liquid transaction was not found, txid={} waiting for broadcast",
tx_id,
)),
}
}
}

View File

@@ -0,0 +1,56 @@
pub(crate) mod electrum;
pub(crate) mod esplora;
use anyhow::Result;
use mockall::automock;
use crate::{
elements,
model::{LBtcHistory, Utxo},
};
use elements::{Address, Script, Transaction, Txid};
pub(crate) type History = LBtcHistory;
#[automock]
#[sdk_macros::async_trait]
pub trait LiquidChainService: Send + Sync {
/// Get the blockchain latest block
async fn tip(&self) -> Result<u32>;
/// Broadcast a transaction
async fn broadcast(&self, tx: &Transaction) -> Result<Txid>;
/// Get a single transaction from its raw hash
async fn get_transaction_hex(&self, txid: &Txid) -> Result<Option<Transaction>>;
/// Get a list of transactions
async fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>>;
/// Get the transactions involved in a script
async fn get_script_history(&self, scripts: &Script) -> Result<Vec<History>>;
/// Get the transactions involved in a list of scripts.
///
/// The data is fetched in a single call from the Electrum endpoint.
async fn get_scripts_history(&self, scripts: &[Script]) -> Result<Vec<Vec<History>>>;
/// Get the transactions involved in a list of scripts
async fn get_script_history_with_retry(
&self,
script: &Script,
retries: u64,
) -> Result<Vec<History>>;
/// Get the utxos associated with a script pubkey
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>>;
/// Verify that a transaction appears in the address script history
async fn verify_tx(
&self,
address: &Address,
tx_id: &str,
tx_hex: &str,
verify_confirmation: bool,
) -> Result<Transaction>;
}

View File

@@ -6,24 +6,21 @@ use boltz_client::{
swaps::boltz::{ChainSwapStates, CreateChainResponse, TransactionInfo}, swaps::boltz::{ChainSwapStates, CreateChainResponse, TransactionInfo},
ElementsLockTime, Secp256k1, Serialize, ToHex, ElementsLockTime, Secp256k1, Serialize, ToHex,
}; };
use elements::{hex::FromHex, Script, Transaction};
use futures_util::TryFutureExt; use futures_util::TryFutureExt;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use lwk_wollet::{ use lwk_wollet::hashes::hex::DisplayHex;
elements::{hex::FromHex, Script, Transaction},
hashes::hex::DisplayHex,
History,
};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use crate::model::{BlockListener, ChainSwapUpdate, LIQUID_FEE_RATE_MSAT_PER_VBYTE};
use crate::{ use crate::{
chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService}, chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
ensure_sdk, elements, ensure_sdk,
error::{PaymentError, SdkError, SdkResult}, error::{PaymentError, SdkError, SdkResult},
model::{ model::{
ChainSwap, Config, Direction, BlockListener, BtcHistory, ChainSwap, ChainSwapUpdate, Config, Direction, LBtcHistory,
PaymentState::{self, *}, PaymentState::{self, *},
PaymentTxData, PaymentType, Swap, SwapScriptV2, Transaction as SdkTransaction, PaymentTxData, PaymentType, Swap, SwapScriptV2, Transaction as SdkTransaction,
LIQUID_FEE_RATE_MSAT_PER_VBYTE,
}, },
persist::Persister, persist::Persister,
swapper::Swapper, swapper::Swapper,
@@ -165,6 +162,24 @@ impl ChainSwapHandler {
Ok(()) Ok(())
} }
async fn fetch_script_history(&self, swap_script: &SwapScriptV2) -> Result<Vec<(String, i32)>> {
let history = match swap_script {
SwapScriptV2::Liquid(_) => self
.fetch_liquid_script_history(swap_script)
.await?
.into_iter()
.map(|h| (h.txid.to_hex(), h.height))
.collect(),
SwapScriptV2::Bitcoin(_) => self
.fetch_bitcoin_script_history(swap_script)
.await?
.into_iter()
.map(|h| (h.txid.to_hex(), h.height))
.collect(),
};
Ok(history)
}
async fn claim_confirmed_server_lockup(&self, swap: &ChainSwap) -> Result<()> { async fn claim_confirmed_server_lockup(&self, swap: &ChainSwap) -> Result<()> {
let Some(tx_id) = swap.server_lockup_tx_id.clone() else { let Some(tx_id) = swap.server_lockup_tx_id.clone() else {
// Skip the rescan if there is no server_lockup_tx_id yet // Skip the rescan if there is no server_lockup_tx_id yet
@@ -172,17 +187,15 @@ impl ChainSwapHandler {
}; };
let swap_id = &swap.id; let swap_id = &swap.id;
let swap_script = swap.get_claim_swap_script()?; let swap_script = swap.get_claim_swap_script()?;
let script_history = match swap.direction { let script_history = self.fetch_script_history(&swap_script).await?;
Direction::Incoming => self.fetch_liquid_script_history(&swap_script).await, let (_tx_history, tx_height) =
Direction::Outgoing => self.fetch_bitcoin_script_history(&swap_script).await, script_history
}?; .iter()
let tx_history = script_history .find(|h| h.0.eq(&tx_id))
.iter() .ok_or(anyhow!(
.find(|h| h.txid.to_hex().eq(&tx_id)) "Server lockup tx for Chain Swap {swap_id} was not found, txid={tx_id}"
.ok_or(anyhow!( ))?;
"Server lockup tx for Chain Swap {swap_id} was not found, txid={tx_id}" if *tx_height > 0 {
))?;
if tx_history.height > 0 {
info!("Chain Swap {swap_id} server lockup tx is confirmed"); info!("Chain Swap {swap_id} server lockup tx is confirmed");
self.claim(swap_id) self.claim(swap_id)
.await .await
@@ -869,6 +882,7 @@ impl ChainSwapHandler {
SdkTransaction::Bitcoin(tx) => self SdkTransaction::Bitcoin(tx) => self
.bitcoin_chain_service .bitcoin_chain_service
.broadcast(&tx) .broadcast(&tx)
.await
.map(|tx_id| tx_id.to_hex()) .map(|tx_id| tx_id.to_hex())
.map_err(|err| PaymentError::Generic { .map_err(|err| PaymentError::Generic {
err: err.to_string(), err: err.to_string(),
@@ -996,7 +1010,10 @@ impl ChainSwapHandler {
.to_address(self.config.network.as_bitcoin_chain()) .to_address(self.config.network.as_bitcoin_chain())
.map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))? .map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
.script_pubkey(); .script_pubkey();
let utxos = self.bitcoin_chain_service.get_script_utxos(&script_pk)?; let utxos = self
.bitcoin_chain_service
.get_script_utxos(&script_pk)
.await?;
let SdkTransaction::Bitcoin(refund_tx) = self let SdkTransaction::Bitcoin(refund_tx) = self
.swapper .swapper
@@ -1015,7 +1032,8 @@ impl ChainSwapHandler {
}; };
let refund_tx_id = self let refund_tx_id = self
.bitcoin_chain_service .bitcoin_chain_service
.broadcast(&refund_tx)? .broadcast(&refund_tx)
.await?
.to_string(); .to_string();
info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}"); info!("Successfully broadcast refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}");
@@ -1229,7 +1247,8 @@ impl ChainSwapHandler {
// Get full transaction // Get full transaction
let txs = self let txs = self
.bitcoin_chain_service .bitcoin_chain_service
.get_transactions(&[first_tx_id])?; .get_transactions(&[first_tx_id])
.await?;
let user_lockup_tx = txs.first().ok_or(anyhow!( let user_lockup_tx = txs.first().ok_or(anyhow!(
"No transactions found for user lockup script for swap {}", "No transactions found for user lockup script for swap {}",
chain_swap.id chain_swap.id
@@ -1375,27 +1394,21 @@ impl ChainSwapHandler {
} }
async fn user_lockup_tx_exists(&self, chain_swap: &ChainSwap) -> Result<bool> { async fn user_lockup_tx_exists(&self, chain_swap: &ChainSwap) -> Result<bool> {
let swap_script = chain_swap.get_lockup_swap_script()?; let lockup_script = chain_swap.get_lockup_swap_script()?;
let script_history = match chain_swap.direction { let script_history = self.fetch_script_history(&lockup_script).await?;
Direction::Incoming => self.fetch_bitcoin_script_history(&swap_script).await,
Direction::Outgoing => self.fetch_liquid_script_history(&swap_script).await,
}?;
match chain_swap.user_lockup_tx_id.clone() { match chain_swap.user_lockup_tx_id.clone() {
Some(user_lockup_tx_id) => { Some(user_lockup_tx_id) => {
if !script_history if !script_history.iter().any(|h| h.0 == user_lockup_tx_id) {
.iter()
.any(|h| h.txid.to_hex() == user_lockup_tx_id)
{
return Ok(false); return Ok(false);
} }
} }
None => { None => {
let txid = match script_history.first() { let (txid, _tx_height) = match script_history.into_iter().nth(0) {
Some(h) => h,
None => { None => {
return Ok(false); return Ok(false);
} }
Some(h) => h.txid.to_hex(),
}; };
self.update_swap_info(&ChainSwapUpdate { self.update_swap_info(&ChainSwapUpdate {
swap_id: chain_swap.id.clone(), swap_id: chain_swap.id.clone(),
@@ -1441,7 +1454,7 @@ impl ChainSwapHandler {
async fn fetch_bitcoin_script_history( async fn fetch_bitcoin_script_history(
&self, &self,
swap_script: &SwapScriptV2, swap_script: &SwapScriptV2,
) -> Result<Vec<History>> { ) -> Result<Vec<BtcHistory>> {
let address = swap_script let address = swap_script
.as_bitcoin_script()? .as_bitcoin_script()?
.to_address(self.config.network.as_bitcoin_chain()) .to_address(self.config.network.as_bitcoin_chain())
@@ -1456,7 +1469,7 @@ impl ChainSwapHandler {
async fn fetch_liquid_script_history( async fn fetch_liquid_script_history(
&self, &self,
swap_script: &SwapScriptV2, swap_script: &SwapScriptV2,
) -> Result<Vec<History>> { ) -> Result<Vec<LBtcHistory>> {
let address = swap_script let address = swap_script
.as_liquid_script()? .as_liquid_script()?
.to_address(self.config.network.into()) .to_address(self.config.network.into())

View File

@@ -233,8 +233,8 @@ impl From<SdkError> for PaymentError {
} }
} }
impl From<crate::bitcoin::util::bip32::Error> for PaymentError { impl From<sdk_common::bitcoin::util::bip32::Error> for PaymentError {
fn from(err: crate::bitcoin::util::bip32::Error) -> Self { fn from(err: sdk_common::bitcoin::util::bip32::Error) -> Self {
Self::SignerError { Self::SignerError {
err: err.to_string(), err: err.to_string(),
} }

View File

@@ -2476,6 +2476,30 @@ impl SseDecode for crate::bindings::BitcoinAddressData {
} }
} }
impl SseDecode for crate::model::BlockchainExplorer {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut tag_ = <i32>::sse_decode(deserializer);
match tag_ {
0 => {
let mut var_url = <String>::sse_decode(deserializer);
return crate::model::BlockchainExplorer::Electrum { url: var_url };
}
1 => {
let mut var_url = <String>::sse_decode(deserializer);
let mut var_useWaterfalls = <bool>::sse_decode(deserializer);
return crate::model::BlockchainExplorer::Esplora {
url: var_url,
use_waterfalls: var_useWaterfalls,
};
}
_ => {
unimplemented!("");
}
}
}
}
impl SseDecode for crate::model::BlockchainInfo { impl SseDecode for crate::model::BlockchainInfo {
// 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 {
@@ -2546,9 +2570,8 @@ impl SseDecode for crate::model::CheckMessageResponse {
impl SseDecode for crate::model::Config { impl SseDecode for crate::model::Config {
// 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_liquidElectrumUrl = <String>::sse_decode(deserializer); let mut var_liquidExplorer = <crate::model::BlockchainExplorer>::sse_decode(deserializer);
let mut var_bitcoinElectrumUrl = <String>::sse_decode(deserializer); let mut var_bitcoinExplorer = <crate::model::BlockchainExplorer>::sse_decode(deserializer);
let mut var_mempoolspaceUrl = <String>::sse_decode(deserializer);
let mut var_workingDir = <String>::sse_decode(deserializer); let mut var_workingDir = <String>::sse_decode(deserializer);
let mut var_cacheDir = <Option<String>>::sse_decode(deserializer); let mut var_cacheDir = <Option<String>>::sse_decode(deserializer);
let mut var_network = <crate::model::LiquidNetwork>::sse_decode(deserializer); let mut var_network = <crate::model::LiquidNetwork>::sse_decode(deserializer);
@@ -2563,9 +2586,8 @@ impl SseDecode for crate::model::Config {
let mut var_assetMetadata = let mut var_assetMetadata =
<Option<Vec<crate::model::AssetMetadata>>>::sse_decode(deserializer); <Option<Vec<crate::model::AssetMetadata>>>::sse_decode(deserializer);
return crate::model::Config { return crate::model::Config {
liquid_electrum_url: var_liquidElectrumUrl, liquid_explorer: var_liquidExplorer,
bitcoin_electrum_url: var_bitcoinElectrumUrl, bitcoin_explorer: var_bitcoinExplorer,
mempoolspace_url: var_mempoolspaceUrl,
working_dir: var_workingDir, working_dir: var_workingDir,
cache_dir: var_cacheDir, cache_dir: var_cacheDir,
network: var_network, network: var_network,
@@ -5055,6 +5077,39 @@ impl flutter_rust_bridge::IntoIntoDart<FrbWrapper<crate::bindings::BitcoinAddres
} }
} }
// 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::BlockchainExplorer {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
crate::model::BlockchainExplorer::Electrum { url } => {
[0.into_dart(), url.into_into_dart().into_dart()].into_dart()
}
crate::model::BlockchainExplorer::Esplora {
url,
use_waterfalls,
} => [
1.into_dart(),
url.into_into_dart().into_dart(),
use_waterfalls.into_into_dart().into_dart(),
]
.into_dart(),
_ => {
unimplemented!("");
}
}
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::model::BlockchainExplorer
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::model::BlockchainExplorer>
for crate::model::BlockchainExplorer
{
fn into_into_dart(self) -> crate::model::BlockchainExplorer {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::BlockchainInfo { impl flutter_rust_bridge::IntoDart for crate::model::BlockchainInfo {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
@@ -5156,9 +5211,8 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::CheckMessageResponse>
impl flutter_rust_bridge::IntoDart for crate::model::Config { impl flutter_rust_bridge::IntoDart for crate::model::Config {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[ [
self.liquid_electrum_url.into_into_dart().into_dart(), self.liquid_explorer.into_into_dart().into_dart(),
self.bitcoin_electrum_url.into_into_dart().into_dart(), self.bitcoin_explorer.into_into_dart().into_dart(),
self.mempoolspace_url.into_into_dart().into_dart(),
self.working_dir.into_into_dart().into_dart(), self.working_dir.into_into_dart().into_dart(),
self.cache_dir.into_into_dart().into_dart(), self.cache_dir.into_into_dart().into_dart(),
self.network.into_into_dart().into_dart(), self.network.into_into_dart().into_dart(),
@@ -7436,6 +7490,29 @@ impl SseEncode for crate::bindings::BitcoinAddressData {
} }
} }
impl SseEncode for crate::model::BlockchainExplorer {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
match self {
crate::model::BlockchainExplorer::Electrum { url } => {
<i32>::sse_encode(0, serializer);
<String>::sse_encode(url, serializer);
}
crate::model::BlockchainExplorer::Esplora {
url,
use_waterfalls,
} => {
<i32>::sse_encode(1, serializer);
<String>::sse_encode(url, serializer);
<bool>::sse_encode(use_waterfalls, serializer);
}
_ => {
unimplemented!("");
}
}
}
}
impl SseEncode for crate::model::BlockchainInfo { impl SseEncode for crate::model::BlockchainInfo {
// 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) {
@@ -7493,9 +7570,8 @@ impl SseEncode for crate::model::CheckMessageResponse {
impl SseEncode for crate::model::Config { impl SseEncode for crate::model::Config {
// 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) {
<String>::sse_encode(self.liquid_electrum_url, serializer); <crate::model::BlockchainExplorer>::sse_encode(self.liquid_explorer, serializer);
<String>::sse_encode(self.bitcoin_electrum_url, serializer); <crate::model::BlockchainExplorer>::sse_encode(self.bitcoin_explorer, serializer);
<String>::sse_encode(self.mempoolspace_url, serializer);
<String>::sse_encode(self.working_dir, serializer); <String>::sse_encode(self.working_dir, serializer);
<Option<String>>::sse_encode(self.cache_dir, serializer); <Option<String>>::sse_encode(self.cache_dir, serializer);
<crate::model::LiquidNetwork>::sse_encode(self.network, serializer); <crate::model::LiquidNetwork>::sse_encode(self.network, serializer);
@@ -9491,6 +9567,27 @@ mod io {
} }
} }
} }
impl CstDecode<crate::model::BlockchainExplorer> for wire_cst_blockchain_explorer {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::BlockchainExplorer {
match self.tag {
0 => {
let ans = unsafe { self.kind.Electrum };
crate::model::BlockchainExplorer::Electrum {
url: ans.url.cst_decode(),
}
}
1 => {
let ans = unsafe { self.kind.Esplora };
crate::model::BlockchainExplorer::Esplora {
url: ans.url.cst_decode(),
use_waterfalls: ans.use_waterfalls.cst_decode(),
}
}
_ => unreachable!(),
}
}
}
impl CstDecode<crate::model::BlockchainInfo> for wire_cst_blockchain_info { impl CstDecode<crate::model::BlockchainInfo> for wire_cst_blockchain_info {
// 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::BlockchainInfo { fn cst_decode(self) -> crate::model::BlockchainInfo {
@@ -9930,9 +10027,8 @@ mod io {
// 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::Config { fn cst_decode(self) -> crate::model::Config {
crate::model::Config { crate::model::Config {
liquid_electrum_url: self.liquid_electrum_url.cst_decode(), liquid_explorer: self.liquid_explorer.cst_decode(),
bitcoin_electrum_url: self.bitcoin_electrum_url.cst_decode(), bitcoin_explorer: self.bitcoin_explorer.cst_decode(),
mempoolspace_url: self.mempoolspace_url.cst_decode(),
working_dir: self.working_dir.cst_decode(), working_dir: self.working_dir.cst_decode(),
cache_dir: self.cache_dir.cst_decode(), cache_dir: self.cache_dir.cst_decode(),
network: self.network.cst_decode(), network: self.network.cst_decode(),
@@ -11575,6 +11671,19 @@ mod io {
Self::new_with_null_ptr() Self::new_with_null_ptr()
} }
} }
impl NewWithNullPtr for wire_cst_blockchain_explorer {
fn new_with_null_ptr() -> Self {
Self {
tag: -1,
kind: BlockchainExplorerKind { nil__: () },
}
}
}
impl Default for wire_cst_blockchain_explorer {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_blockchain_info { impl NewWithNullPtr for wire_cst_blockchain_info {
fn new_with_null_ptr() -> Self { fn new_with_null_ptr() -> Self {
Self { Self {
@@ -11630,9 +11739,8 @@ mod io {
impl NewWithNullPtr for wire_cst_config { impl NewWithNullPtr for wire_cst_config {
fn new_with_null_ptr() -> Self { fn new_with_null_ptr() -> Self {
Self { Self {
liquid_electrum_url: core::ptr::null_mut(), liquid_explorer: Default::default(),
bitcoin_electrum_url: core::ptr::null_mut(), bitcoin_explorer: Default::default(),
mempoolspace_url: core::ptr::null_mut(),
working_dir: core::ptr::null_mut(), working_dir: core::ptr::null_mut(),
cache_dir: core::ptr::null_mut(), cache_dir: core::ptr::null_mut(),
network: Default::default(), network: Default::default(),
@@ -13859,6 +13967,30 @@ mod io {
} }
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_blockchain_explorer {
tag: i32,
kind: BlockchainExplorerKind,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union BlockchainExplorerKind {
Electrum: wire_cst_BlockchainExplorer_Electrum,
Esplora: wire_cst_BlockchainExplorer_Esplora,
nil__: (),
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_BlockchainExplorer_Electrum {
url: *mut wire_cst_list_prim_u_8_strict,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_BlockchainExplorer_Esplora {
url: *mut wire_cst_list_prim_u_8_strict,
use_waterfalls: bool,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_blockchain_info { pub struct wire_cst_blockchain_info {
liquid_tip: u32, liquid_tip: u32,
bitcoin_tip: u32, bitcoin_tip: u32,
@@ -13884,9 +14016,8 @@ mod io {
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct wire_cst_config { pub struct wire_cst_config {
liquid_electrum_url: *mut wire_cst_list_prim_u_8_strict, liquid_explorer: wire_cst_blockchain_explorer,
bitcoin_electrum_url: *mut wire_cst_list_prim_u_8_strict, bitcoin_explorer: wire_cst_blockchain_explorer,
mempoolspace_url: *mut wire_cst_list_prim_u_8_strict,
working_dir: *mut wire_cst_list_prim_u_8_strict, working_dir: *mut wire_cst_list_prim_u_8_strict,
cache_dir: *mut wire_cst_list_prim_u_8_strict, cache_dir: *mut wire_cst_list_prim_u_8_strict,
network: i32, network: i32,

View File

@@ -189,6 +189,8 @@ pub(crate) mod test_utils;
pub(crate) mod utils; pub(crate) mod utils;
pub mod wallet; pub mod wallet;
pub use lwk_wollet::bitcoin;
pub use lwk_wollet::elements;
pub use sdk_common::prelude::*; pub use sdk_common::prelude::*;
#[allow(ambiguous_glob_reexports)] #[allow(ambiguous_glob_reexports)]

View File

@@ -1,44 +1,75 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use boltz_client::{ use boltz_client::{
bitcoin::ScriptBuf,
boltz::{ChainPair, BOLTZ_MAINNET_URL_V2, BOLTZ_REGTEST, BOLTZ_TESTNET_URL_V2}, boltz::{ChainPair, BOLTZ_MAINNET_URL_V2, BOLTZ_REGTEST, BOLTZ_TESTNET_URL_V2},
network::{BitcoinChain, Chain, LiquidChain}, network::{BitcoinChain, Chain, LiquidChain},
swaps::boltz::{ swaps::boltz::{
CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree, CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse, Leaf, Side, SwapTree,
}, },
ToHex, BtcSwapScript, Keypair, LBtcSwapScript,
}; };
use boltz_client::{BtcSwapScript, Keypair, LBtcSwapScript};
use derivative::Derivative; use derivative::Derivative;
use lwk_wollet::elements::{script, AssetId};
use lwk_wollet::{bitcoin::bip32, ElementsNetwork};
use maybe_sync::{MaybeSend, MaybeSync}; use maybe_sync::{MaybeSend, MaybeSync};
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::ToSql; use rusqlite::ToSql;
use sdk_common::prelude::*; use sdk_common::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::PartialEq;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::{cmp::PartialEq, sync::Arc};
use strum_macros::{Display, EnumString}; use strum_macros::{Display, EnumString};
use crate::error::{PaymentError, SdkError, SdkResult};
use crate::prelude::DEFAULT_EXTERNAL_INPUT_PARSERS;
use crate::receive_swap::DEFAULT_ZERO_CONF_MAX_SAT; use crate::receive_swap::DEFAULT_ZERO_CONF_MAX_SAT;
use crate::utils; use crate::utils;
use crate::{
bitcoin,
chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
elements,
error::{PaymentError, SdkError, SdkResult},
};
use crate::{
chain::{
bitcoin::{electrum::ElectrumBitcoinChainService, esplora::EsploraBitcoinChainService},
liquid::{electrum::ElectrumLiquidChainService, esplora::EsploraLiquidChainService},
},
prelude::DEFAULT_EXTERNAL_INPUT_PARSERS,
};
use bitcoin::{bip32, ScriptBuf};
use elements::AssetId;
use lwk_wollet::ElementsNetwork;
use sdk_common::bitcoin::hashes::hex::ToHex as _;
// Uses f64 for the maximum precision when converting between units // Uses f64 for the maximum precision when converting between units
pub const LIQUID_FEE_RATE_SAT_PER_VBYTE: f64 = 0.1; pub const LIQUID_FEE_RATE_SAT_PER_VBYTE: f64 = 0.1;
pub const LIQUID_FEE_RATE_MSAT_PER_VBYTE: f32 = (LIQUID_FEE_RATE_SAT_PER_VBYTE * 1000.0) as f32; pub const LIQUID_FEE_RATE_MSAT_PER_VBYTE: f32 = (LIQUID_FEE_RATE_SAT_PER_VBYTE * 1000.0) as f32;
pub const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology"; pub const BREEZ_SYNC_SERVICE_URL: &str = "https://datasync.breez.technology";
#[derive(Clone, Debug, Serialize)]
pub enum BlockchainExplorer {
Electrum {
url: String,
},
Esplora {
url: String,
/// Whether or not to use the "waterfalls" extension
use_waterfalls: bool,
},
}
impl BlockchainExplorer {
pub(crate) fn url(&self) -> &String {
match self {
BlockchainExplorer::Electrum { url } => url,
BlockchainExplorer::Esplora { url, .. } => url,
}
}
}
/// Configuration for the Liquid SDK /// Configuration for the Liquid SDK
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct Config { pub struct Config {
pub liquid_electrum_url: String, pub liquid_explorer: BlockchainExplorer,
pub bitcoin_electrum_url: String, pub bitcoin_explorer: BlockchainExplorer,
/// The mempool.space API URL, has to be in the format: `https://mempool.space/api`
pub mempoolspace_url: String,
/// Directory in which the DB and log files are stored. /// Directory in which the DB and log files are stored.
/// ///
/// Prefix can be a relative or absolute path to this directory. /// Prefix can be a relative or absolute path to this directory.
@@ -81,9 +112,12 @@ pub struct Config {
impl Config { impl Config {
pub fn mainnet(breez_api_key: Option<String>) -> Self { pub fn mainnet(breez_api_key: Option<String>) -> Self {
Config { Config {
liquid_electrum_url: "elements-mainnet.breez.technology:50002".to_string(), liquid_explorer: BlockchainExplorer::Electrum {
bitcoin_electrum_url: "bitcoin-mainnet.blockstream.info:50002".to_string(), url: "elements-mainnet.breez.technology:50002".to_string(),
mempoolspace_url: "https://mempool.space/api".to_string(), },
bitcoin_explorer: BlockchainExplorer::Electrum {
url: "bitcoin-mainnet.blockstream.info:50002".to_string(),
},
working_dir: ".".to_string(), working_dir: ".".to_string(),
cache_dir: None, cache_dir: None,
network: LiquidNetwork::Mainnet, network: LiquidNetwork::Mainnet,
@@ -100,9 +134,12 @@ impl Config {
pub fn testnet(breez_api_key: Option<String>) -> Self { pub fn testnet(breez_api_key: Option<String>) -> Self {
Config { Config {
liquid_electrum_url: "elements-testnet.blockstream.info:50002".to_string(), liquid_explorer: BlockchainExplorer::Electrum {
bitcoin_electrum_url: "bitcoin-testnet.blockstream.info:50002".to_string(), url: "elements-testnet.blockstream.info:50002".to_string(),
mempoolspace_url: "https://mempool.space/testnet/api".to_string(), },
bitcoin_explorer: BlockchainExplorer::Electrum {
url: "bitcoin-testnet.blockstream.info:50002".to_string(),
},
working_dir: ".".to_string(), working_dir: ".".to_string(),
cache_dir: None, cache_dir: None,
network: LiquidNetwork::Testnet, network: LiquidNetwork::Testnet,
@@ -119,9 +156,12 @@ impl Config {
pub fn regtest() -> Self { pub fn regtest() -> Self {
Config { Config {
liquid_electrum_url: "localhost:19002".to_string(), bitcoin_explorer: BlockchainExplorer::Electrum {
bitcoin_electrum_url: "localhost:19001".to_string(), url: "localhost:19001".to_string(),
mempoolspace_url: "http://localhost/api".to_string(), },
liquid_explorer: BlockchainExplorer::Electrum {
url: "localhost:19002".to_string(),
},
working_dir: ".".to_string(), working_dir: ".".to_string(),
cache_dir: None, cache_dir: None,
network: LiquidNetwork::Regtest, network: LiquidNetwork::Regtest,
@@ -193,6 +233,28 @@ impl Config {
pub fn sync_enabled(&self) -> bool { pub fn sync_enabled(&self) -> bool {
self.sync_service_url.is_some() self.sync_service_url.is_some()
} }
pub(crate) fn bitcoin_chain_service(&self) -> Arc<dyn BitcoinChainService> {
match self.bitcoin_explorer {
BlockchainExplorer::Esplora { .. } => {
Arc::new(EsploraBitcoinChainService::new(self.clone()))
}
BlockchainExplorer::Electrum { .. } => {
Arc::new(ElectrumBitcoinChainService::new(self.clone()))
}
}
}
pub(crate) fn liquid_chain_service(&self) -> Arc<dyn LiquidChainService> {
match self.liquid_explorer {
BlockchainExplorer::Esplora { .. } => {
Arc::new(EsploraLiquidChainService::new(self.clone()))
}
BlockchainExplorer::Electrum { .. } => {
Arc::new(ElectrumLiquidChainService::new(self.clone()))
}
}
}
} }
/// Network chosen for this Liquid SDK instance. Note that it represents both the Liquid and the /// Network chosen for this Liquid SDK instance. Note that it represents both the Liquid and the
@@ -1248,7 +1310,7 @@ impl ReceiveSwap {
utils::decode_keypair(&self.claim_private_key).map_err(Into::into) utils::decode_keypair(&self.claim_private_key).map_err(Into::into)
} }
pub(crate) fn claim_script(&self) -> Result<script::Script> { pub(crate) fn claim_script(&self) -> Result<elements::Script> {
Ok(self Ok(self
.get_swap_script()? .get_swap_script()?
.funding_addrs .funding_addrs
@@ -2177,6 +2239,67 @@ pub struct AcceptPaymentProposedFeesRequest {
pub response: FetchPaymentProposedFeesResponse, pub response: FetchPaymentProposedFeesResponse,
} }
#[derive(Clone, Debug)]
pub struct History<T> {
pub txid: T,
/// Confirmation height of txid
///
/// -1 means unconfirmed with unconfirmed parents
/// 0 means unconfirmed with confirmed parents
pub height: i32,
}
pub(crate) type LBtcHistory = History<elements::Txid>;
pub(crate) type BtcHistory = History<bitcoin::Txid>;
impl<T> History<T> {
pub(crate) fn confirmed(&self) -> bool {
self.height > 0
}
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
impl From<electrum_client::GetHistoryRes> for BtcHistory {
fn from(value: electrum_client::GetHistoryRes) -> Self {
Self {
txid: value.tx_hash,
height: value.height,
}
}
}
impl From<lwk_wollet::History> for LBtcHistory {
fn from(value: lwk_wollet::History) -> Self {
Self::from(&value)
}
}
impl From<&lwk_wollet::History> for LBtcHistory {
fn from(value: &lwk_wollet::History) -> Self {
Self {
txid: value.txid,
height: value.height,
}
}
}
pub(crate) type BtcScript = bitcoin::ScriptBuf;
pub(crate) type LBtcScript = elements::Script;
#[derive(Clone, Debug)]
pub struct BtcScriptBalance {
/// Confirmed balance in Satoshis for the address.
pub confirmed: u64,
/// Unconfirmed balance in Satoshis for the address.
///
/// Some servers (e.g. `electrs`) return this as a negative value.
pub unconfirmed: i64,
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
impl From<electrum_client::GetBalanceRes> for BtcScriptBalance {
fn from(val: electrum_client::GetBalanceRes) -> Self {
Self {
confirmed: val.confirmed,
unconfirmed: val.unconfirmed,
}
}
}
#[macro_export] #[macro_export]
macro_rules! get_invoice_amount { macro_rules! get_invoice_amount {
($invoice:expr) => { ($invoice:expr) => {

View File

@@ -1,7 +1,6 @@
use anyhow::Result; use anyhow::Result;
use boltz_client::boltz::PairLimits; use boltz_client::boltz::PairLimits;
use boltz_client::ElementsAddress; use boltz_client::ElementsAddress;
use electrum_client::GetBalanceRes;
use log::{debug, warn}; use log::{debug, warn};
use lwk_wollet::elements::{secp256k1_zkp, AddressParams}; use lwk_wollet::elements::{secp256k1_zkp, AddressParams};
use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey; use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey;
@@ -240,7 +239,7 @@ impl ChainReceiveSwapHandler {
.to_sat(); .to_sat();
// Collect outgoing tx IDs // Collect outgoing tx IDs
let btc_outgoing_tx_ids: Vec<HistoryTxId> = btc_lockup_outgoing_txs let btc_outgoing_tx_ids: Vec<BtcHistory> = btc_lockup_outgoing_txs
.iter() .iter()
.filter_map(|tx| { .filter_map(|tx| {
history history
@@ -282,19 +281,19 @@ impl ChainReceiveSwapHandler {
pub(crate) struct RecoveredOnchainDataChainReceive { pub(crate) struct RecoveredOnchainDataChainReceive {
/// LBTC tx locking up funds by the swapper /// LBTC tx locking up funds by the swapper
pub(crate) lbtc_server_lockup_tx_id: Option<HistoryTxId>, pub(crate) lbtc_server_lockup_tx_id: Option<LBtcHistory>,
/// LBTC tx that claims to our wallet. The final step in a successful swap. /// LBTC tx that claims to our wallet. The final step in a successful swap.
pub(crate) lbtc_claim_tx_id: Option<HistoryTxId>, pub(crate) lbtc_claim_tx_id: Option<LBtcHistory>,
/// LBTC tx out address for the claim tx. /// LBTC tx out address for the claim tx.
pub(crate) lbtc_claim_address: Option<String>, pub(crate) lbtc_claim_address: Option<String>,
/// BTC tx initiated by the payer (the "user" as per Boltz), sending funds to the swap funding address. /// BTC tx initiated by the payer (the "user" as per Boltz), sending funds to the swap funding address.
pub(crate) btc_user_lockup_tx_id: Option<HistoryTxId>, pub(crate) btc_user_lockup_tx_id: Option<BtcHistory>,
/// BTC total funds currently available at the swap funding address. /// BTC total funds currently available at the swap funding address.
pub(crate) btc_user_lockup_address_balance_sat: u64, pub(crate) btc_user_lockup_address_balance_sat: u64,
/// BTC sent to lockup address as part of lockup tx. /// BTC sent to lockup address as part of lockup tx.
pub(crate) btc_user_lockup_amount_sat: u64, pub(crate) btc_user_lockup_amount_sat: u64,
/// BTC tx initiated by the SDK to a user-chosen address, in case the initial funds have to be refunded. /// BTC tx initiated by the SDK to a user-chosen address, in case the initial funds have to be refunded.
pub(crate) btc_refund_tx_id: Option<HistoryTxId>, pub(crate) btc_refund_tx_id: Option<BtcHistory>,
} }
impl RecoveredOnchainDataChainReceive { impl RecoveredOnchainDataChainReceive {
@@ -363,8 +362,8 @@ impl RecoveredOnchainDataChainReceive {
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ReceiveChainSwapHistory { pub(crate) struct ReceiveChainSwapHistory {
pub(crate) lbtc_claim_script_history: Vec<HistoryTxId>, pub(crate) lbtc_claim_script_history: Vec<LBtcHistory>,
pub(crate) btc_lockup_script_history: Vec<HistoryTxId>, pub(crate) btc_lockup_script_history: Vec<BtcHistory>,
pub(crate) btc_lockup_script_txs: Vec<boltz_client::bitcoin::Transaction>, pub(crate) btc_lockup_script_txs: Vec<bitcoin::Transaction>,
pub(crate) btc_lockup_script_balance: Option<GetBalanceRes>, pub(crate) btc_lockup_script_balance: Option<BtcScriptBalance>,
} }

View File

@@ -204,13 +204,13 @@ impl ChainSendSwapHandler {
pub(crate) struct RecoveredOnchainDataChainSend { pub(crate) struct RecoveredOnchainDataChainSend {
/// LBTC tx initiated by the SDK (the "user" as per Boltz), sending funds to the swap funding address. /// LBTC tx initiated by the SDK (the "user" as per Boltz), sending funds to the swap funding address.
pub(crate) lbtc_user_lockup_tx_id: Option<HistoryTxId>, pub(crate) lbtc_user_lockup_tx_id: Option<LBtcHistory>,
/// LBTC tx initiated by the SDK to itself, in case the initial funds have to be refunded. /// LBTC tx initiated by the SDK to itself, in case the initial funds have to be refunded.
pub(crate) lbtc_refund_tx_id: Option<HistoryTxId>, pub(crate) lbtc_refund_tx_id: Option<LBtcHistory>,
/// BTC tx locking up funds by the swapper /// BTC tx locking up funds by the swapper
pub(crate) btc_server_lockup_tx_id: Option<HistoryTxId>, pub(crate) btc_server_lockup_tx_id: Option<BtcHistory>,
/// BTC tx that claims to the final BTC destination address. The final step in a successful swap. /// BTC tx that claims to the final BTC destination address. The final step in a successful swap.
pub(crate) btc_claim_tx_id: Option<HistoryTxId>, pub(crate) btc_claim_tx_id: Option<BtcHistory>,
} }
// TODO: We have to be careful around overwriting the RefundPending state, as this swap monitored // TODO: We have to be careful around overwriting the RefundPending state, as this swap monitored
@@ -254,7 +254,7 @@ impl RecoveredOnchainDataChainSend {
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct SendChainSwapHistory { pub(crate) struct SendChainSwapHistory {
pub(crate) lbtc_lockup_script_history: Vec<HistoryTxId>, pub(crate) lbtc_lockup_script_history: Vec<LBtcHistory>,
pub(crate) btc_claim_script_history: Vec<HistoryTxId>, pub(crate) btc_claim_script_history: Vec<BtcHistory>,
pub(crate) btc_claim_script_txs: Vec<boltz_client::bitcoin::Transaction>, pub(crate) btc_claim_script_txs: Vec<bitcoin::Transaction>,
} }

View File

@@ -180,9 +180,9 @@ impl ReceiveSwapHandler {
} }
pub(crate) struct RecoveredOnchainDataReceive { pub(crate) struct RecoveredOnchainDataReceive {
pub(crate) lockup_tx_id: Option<HistoryTxId>, pub(crate) lockup_tx_id: Option<LBtcHistory>,
pub(crate) claim_tx_id: Option<HistoryTxId>, pub(crate) claim_tx_id: Option<LBtcHistory>,
pub(crate) mrh_tx_id: Option<HistoryTxId>, pub(crate) mrh_tx_id: Option<LBtcHistory>,
pub(crate) mrh_amount_sat: Option<u64>, pub(crate) mrh_amount_sat: Option<u64>,
} }
@@ -217,6 +217,6 @@ impl RecoveredOnchainDataReceive {
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ReceiveSwapHistory { pub(crate) struct ReceiveSwapHistory {
pub(crate) lbtc_claim_script_history: Vec<HistoryTxId>, pub(crate) lbtc_claim_script_history: Vec<LBtcHistory>,
pub(crate) lbtc_mrh_script_history: Vec<HistoryTxId>, pub(crate) lbtc_mrh_script_history: Vec<LBtcHistory>,
} }

View File

@@ -52,7 +52,7 @@ impl SendSwapHandler {
.ok_or(anyhow::anyhow!("no funding address found"))? .ok_or(anyhow::anyhow!("no funding address found"))?
.script_pubkey(); .script_pubkey();
let empty_history = Vec::<HistoryTxId>::new(); let empty_history = Vec::<LBtcHistory>::new();
let history = context let history = context
.lbtc_script_to_history_map .lbtc_script_to_history_map
.get(&lockup_script) .get(&lockup_script)
@@ -145,10 +145,10 @@ impl SendSwapHandler {
fn recover_onchain_data( fn recover_onchain_data(
tx_map: &TxMap, tx_map: &TxMap,
swap_id: &str, swap_id: &str,
history: &[HistoryTxId], wallet_history: &[LBtcHistory],
) -> Result<RecoveredOnchainDataSend> { ) -> Result<RecoveredOnchainDataSend> {
// If a history tx is one of our outgoing txs, it's a lockup tx // If a history tx is one of our outgoing txs, it's a lockup tx
let lockup_tx_id = history let lockup_tx_id = wallet_history
.iter() .iter()
.find(|&tx| tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid)) .find(|&tx| tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid))
.cloned(); .cloned();
@@ -158,7 +158,7 @@ impl SendSwapHandler {
// //
// Only find the claim_tx from the history if we find a lockup_tx. Not doing so will select // Only find the claim_tx from the history if we find a lockup_tx. Not doing so will select
// the first tx as the claim, whereas we should check that the claim is not the lockup. // the first tx as the claim, whereas we should check that the claim is not the lockup.
history wallet_history
.iter() .iter()
.filter(|&tx| !tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid)) .filter(|&tx| !tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid))
.find(|&tx| !tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid)) .find(|&tx| !tx_map.outgoing_tx_map.contains_key::<Txid>(&tx.txid))
@@ -169,7 +169,7 @@ impl SendSwapHandler {
}; };
// If a history tx is one of our incoming txs, it's a refund tx // If a history tx is one of our incoming txs, it's a refund tx
let refund_tx_id = history let refund_tx_id = wallet_history
.iter() .iter()
.find(|&tx| tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid)) .find(|&tx| tx_map.incoming_tx_map.contains_key::<Txid>(&tx.txid))
.cloned(); .cloned();
@@ -239,9 +239,9 @@ impl SendSwapHandler {
} }
pub(crate) struct RecoveredOnchainDataSend { pub(crate) struct RecoveredOnchainDataSend {
pub(crate) lockup_tx_id: Option<HistoryTxId>, pub(crate) lockup_tx_id: Option<LBtcHistory>,
pub(crate) claim_tx_id: Option<HistoryTxId>, pub(crate) claim_tx_id: Option<LBtcHistory>,
pub(crate) refund_tx_id: Option<HistoryTxId>, pub(crate) refund_tx_id: Option<LBtcHistory>,
pub(crate) preimage: Option<String>, pub(crate) preimage: Option<String>,
} }

View File

@@ -11,13 +11,14 @@ pub(crate) use self::handle_chain_send_swap::ChainSendSwapHandler;
pub(crate) use self::handle_receive_swap::ReceiveSwapHandler; pub(crate) use self::handle_receive_swap::ReceiveSwapHandler;
pub(crate) use self::handle_send_swap::SendSwapHandler; pub(crate) use self::handle_send_swap::SendSwapHandler;
use super::model::{HistoryTxId, TxMap}; use super::model::TxMap;
use crate::model::LBtcHistory;
/// Helper function for determining lockup and claim transactions in incoming swaps /// Helper function for determining lockup and claim transactions in incoming swaps
pub(crate) fn determine_incoming_lockup_and_claim_txs( pub(crate) fn determine_incoming_lockup_and_claim_txs(
history: &[HistoryTxId], history: &[LBtcHistory],
tx_map: &TxMap, tx_map: &TxMap,
) -> (Option<HistoryTxId>, Option<HistoryTxId>) { ) -> (Option<LBtcHistory>, Option<LBtcHistory>) {
match history.len() { match history.len() {
// Only lockup tx available // Only lockup tx available
1 => (Some(history[0].clone()), None), 1 => (Some(history[0].clone()), None),

View File

@@ -4,7 +4,7 @@ mod test {
model::PaymentState, model::PaymentState,
recover::handlers::{ recover::handlers::{
handle_chain_receive_swap::RecoveredOnchainDataChainReceive, handle_chain_receive_swap::RecoveredOnchainDataChainReceive,
tests::test::create_history_txid, tests::{create_btc_history_txid, create_lbtc_history_txid},
}, },
}; };
use boltz_client::boltz::PairLimits; use boltz_client::boltz::PairLimits;
@@ -15,10 +15,10 @@ mod test {
#[sdk_macros::test_all] #[sdk_macros::test_all]
fn test_derive_partial_state_with_btc_lockup_and_lbtc_claim() { fn test_derive_partial_state_with_btc_lockup_and_lbtc_claim() {
let recovered_data = RecoveredOnchainDataChainReceive { let recovered_data = RecoveredOnchainDataChainReceive {
lbtc_server_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_server_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_claim_tx_id: Some(create_history_txid("2222", 101)), lbtc_claim_tx_id: Some(create_lbtc_history_txid("2222", 101)),
lbtc_claim_address: Some("lq1qqvynd50t4tajashdguell7nu9gycuqqd869w8vqww9ys9dsz7szdfeu7pwe4yzzme28qsluyfyrtqmq9scl5ydw4lesx3c5qu".to_string()), lbtc_claim_address: Some("lq1qqvynd50t4tajashdguell7nu9gycuqqd869w8vqww9ys9dsz7szdfeu7pwe4yzzme28qsluyfyrtqmq9scl5ydw4lesx3c5qu".to_string()),
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 0, btc_user_lockup_address_balance_sat: 0,
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -36,10 +36,10 @@ mod test {
// Test with unconfirmed claim // Test with unconfirmed claim
let recovered_data = RecoveredOnchainDataChainReceive { let recovered_data = RecoveredOnchainDataChainReceive {
lbtc_server_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_server_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_claim_tx_id: Some(create_history_txid("2222", 0)), // Unconfirmed claim lbtc_claim_tx_id: Some(create_lbtc_history_txid("2222", 0)), // Unconfirmed claim
lbtc_claim_address: Some("lq1qqvynd50t4tajashdguell7nu9gycuqqd869w8vqww9ys9dsz7szdfeu7pwe4yzzme28qsluyfyrtqmq9scl5ydw4lesx3c5qu".to_string()), lbtc_claim_address: Some("lq1qqvynd50t4tajashdguell7nu9gycuqqd869w8vqww9ys9dsz7szdfeu7pwe4yzzme28qsluyfyrtqmq9scl5ydw4lesx3c5qu".to_string()),
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 0, btc_user_lockup_address_balance_sat: 0,
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -60,13 +60,13 @@ mod test {
fn test_derive_partial_state_with_btc_lockup_and_btc_refund() { fn test_derive_partial_state_with_btc_lockup_and_btc_refund() {
// Test with confirmed refund // Test with confirmed refund
let recovered_data = RecoveredOnchainDataChainReceive { let recovered_data = RecoveredOnchainDataChainReceive {
lbtc_server_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_server_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 0, btc_user_lockup_address_balance_sat: 0,
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: Some(create_history_txid("4444", 103)), // Confirmed refund btc_refund_tx_id: Some(create_btc_history_txid("4444", 103)), // Confirmed refund
}; };
// When there's a lockup and confirmed refund tx, it should be Failed // When there's a lockup and confirmed refund tx, it should be Failed
@@ -81,13 +81,13 @@ mod test {
// Test with unconfirmed refund // Test with unconfirmed refund
let recovered_data = RecoveredOnchainDataChainReceive { let recovered_data = RecoveredOnchainDataChainReceive {
lbtc_server_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_server_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 0, btc_user_lockup_address_balance_sat: 0,
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: Some(create_history_txid("4444", 0)), // Unconfirmed refund btc_refund_tx_id: Some(create_btc_history_txid("4444", 0)), // Unconfirmed refund
}; };
// When there's a lockup and unconfirmed refund tx, it should be RefundPending // When there's a lockup and unconfirmed refund tx, it should be RefundPending
@@ -108,7 +108,7 @@ mod test {
lbtc_server_lockup_tx_id: None, lbtc_server_lockup_tx_id: None,
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 0, btc_user_lockup_address_balance_sat: 0,
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -135,7 +135,7 @@ mod test {
lbtc_server_lockup_tx_id: None, lbtc_server_lockup_tx_id: None,
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 100000, // Funds still in address btc_user_lockup_address_balance_sat: 100000, // Funds still in address
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -161,7 +161,7 @@ mod test {
lbtc_server_lockup_tx_id: None, lbtc_server_lockup_tx_id: None,
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 5000, btc_user_lockup_address_balance_sat: 5000,
btc_user_lockup_amount_sat: 5000, // Below minimum btc_user_lockup_amount_sat: 5000, // Below minimum
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -178,7 +178,7 @@ mod test {
lbtc_server_lockup_tx_id: None, lbtc_server_lockup_tx_id: None,
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 3000000, btc_user_lockup_address_balance_sat: 3000000,
btc_user_lockup_amount_sat: 3000000, // Above maximum btc_user_lockup_amount_sat: 3000000, // Above maximum
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -195,7 +195,7 @@ mod test {
lbtc_server_lockup_tx_id: None, lbtc_server_lockup_tx_id: None,
lbtc_claim_tx_id: None, lbtc_claim_tx_id: None,
lbtc_claim_address: None, lbtc_claim_address: None,
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 150000, btc_user_lockup_address_balance_sat: 150000,
btc_user_lockup_amount_sat: 150000, // Different from expected btc_user_lockup_amount_sat: 150000, // Different from expected
btc_refund_tx_id: None, btc_refund_tx_id: None,
@@ -237,13 +237,13 @@ mod test {
fn test_derive_partial_state_with_lockup_claim_refund() { fn test_derive_partial_state_with_lockup_claim_refund() {
// This is an edge case where both claim and refund txs exist // This is an edge case where both claim and refund txs exist
let recovered_data = RecoveredOnchainDataChainReceive { let recovered_data = RecoveredOnchainDataChainReceive {
lbtc_server_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_server_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_claim_tx_id: Some(create_history_txid("2222", 101)), lbtc_claim_tx_id: Some(create_lbtc_history_txid("2222", 101)),
lbtc_claim_address: Some("lq1qqvynd50t4tajashdguell7nu9gycuqqd869w8vqww9ys9dsz7szdfeu7pwe4yzzme28qsluyfyrtqmq9scl5ydw4lesx3c5qu".to_string()), lbtc_claim_address: Some("lq1qqvynd50t4tajashdguell7nu9gycuqqd869w8vqww9ys9dsz7szdfeu7pwe4yzzme28qsluyfyrtqmq9scl5ydw4lesx3c5qu".to_string()),
btc_user_lockup_tx_id: Some(create_history_txid("3333", 102)), btc_user_lockup_tx_id: Some(create_btc_history_txid("3333", 102)),
btc_user_lockup_address_balance_sat: 0, btc_user_lockup_address_balance_sat: 0,
btc_user_lockup_amount_sat: 100000, btc_user_lockup_amount_sat: 100000,
btc_refund_tx_id: Some(create_history_txid("4444", 103)), btc_refund_tx_id: Some(create_btc_history_txid("4444", 103)),
}; };
// Complete state should take precedence over refund // Complete state should take precedence over refund

View File

@@ -1,23 +1,18 @@
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
chain::liquid::MockLiquidChainService, bitcoin, elements,
model::{ChainSwap, PaymentState, SwapMetadata}, model::{BtcHistory, BtcScriptBalance, ChainSwap, LBtcHistory, PaymentState, SwapMetadata},
recover::{ recover::{
handlers::{tests::test::create_mock_lbtc_wallet_tx, ChainReceiveSwapHandler}, handlers::{tests::create_mock_lbtc_wallet_tx, ChainReceiveSwapHandler},
model::{HistoryTxId, RecoveryContext, TxMap}, model::{RecoveryContext, TxMap},
}, },
swapper::MockSwapper, swapper::MockSwapper,
test_utils::chain::MockLiquidChainService,
}; };
use boltz_client::{ use bitcoin::{transaction::Version, Sequence};
bitcoin::{self, transaction::Version, Sequence}, use boltz_client::{Amount, LockTime};
Amount, LockTime, use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey;
};
use electrum_client::GetBalanceRes;
use lwk_wollet::{
elements::{self, Txid},
elements_miniscript::slip77::MasterBlindingKey,
};
use std::{collections::HashMap, str::FromStr, sync::Arc}; use std::{collections::HashMap, str::FromStr, sync::Arc};
#[cfg(all(target_family = "wasm", target_os = "unknown"))] #[cfg(all(target_family = "wasm", target_os = "unknown"))]
@@ -204,7 +199,7 @@ mod test {
// Add balance to the lockup address to simulate funds still there // Add balance to the lockup address to simulate funds still there
recovery_context.btc_script_to_balance_map.insert( recovery_context.btc_script_to_balance_map.insert(
btc_lockup_script.clone(), btc_lockup_script.clone(),
GetBalanceRes { BtcScriptBalance {
confirmed: chain_swap.payer_amount_sat, confirmed: chain_swap.payer_amount_sat,
unconfirmed: 0, unconfirmed: 0,
}, },
@@ -416,7 +411,7 @@ mod test {
let computed_txid_str = computed_txid.to_string(); let computed_txid_str = computed_txid.to_string();
// Create history tx with the computed txid // Create history tx with the computed txid
let history_tx = HistoryTxId { let history_tx = BtcHistory {
txid: computed_txid.to_string().parse().unwrap(), txid: computed_txid.to_string().parse().unwrap(),
height: height as i32, height: height as i32,
}; };
@@ -446,7 +441,7 @@ mod test {
// Set balance to 0 (funds have been used) // Set balance to 0 (funds have been used)
context.btc_script_to_balance_map.insert( context.btc_script_to_balance_map.insert(
lockup_script.clone(), lockup_script.clone(),
GetBalanceRes { BtcScriptBalance {
confirmed: 0, confirmed: 0,
unconfirmed: 0, unconfirmed: 0,
}, },
@@ -488,14 +483,8 @@ mod test {
}; };
// Create history tx for refund // Create history tx for refund
let refund_bitcoin_txid: Txid = refund_tx let refund_history_tx = BtcHistory {
.clone() txid: refund_tx.compute_txid(),
.compute_txid()
.to_string()
.parse()
.unwrap();
let refund_history_tx = HistoryTxId {
txid: refund_bitcoin_txid,
height: refund_height as i32, height: refund_height as i32,
}; };
// Add refund tx to script history // Add refund tx to script history
@@ -536,7 +525,7 @@ mod test {
let mut history = Vec::new(); let mut history = Vec::new();
for (tx_id_hex, height) in tx_ids { for (tx_id_hex, height) in tx_ids {
let tx_id = elements::Txid::from_str(tx_id_hex).unwrap(); let tx_id = elements::Txid::from_str(tx_id_hex).unwrap();
history.push(HistoryTxId { history.push(LBtcHistory {
txid: tx_id, txid: tx_id,
height: *height as i32, height: *height as i32,
}); });

View File

@@ -3,7 +3,8 @@ mod test {
use crate::{ use crate::{
model::PaymentState, model::PaymentState,
recover::handlers::{ recover::handlers::{
handle_chain_send_swap::RecoveredOnchainDataChainSend, tests::test::create_history_txid, handle_chain_send_swap::RecoveredOnchainDataChainSend,
tests::{create_btc_history_txid, create_lbtc_history_txid},
}, },
}; };
@@ -13,10 +14,10 @@ mod test {
#[sdk_macros::test_all] #[sdk_macros::test_all]
fn test_derive_partial_state_with_lbtc_lockup_and_btc_claim() { fn test_derive_partial_state_with_lbtc_lockup_and_btc_claim() {
let recovered_data = RecoveredOnchainDataChainSend { let recovered_data = RecoveredOnchainDataChainSend {
lbtc_user_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_user_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_refund_tx_id: None, lbtc_refund_tx_id: None,
btc_server_lockup_tx_id: Some(create_history_txid("2222", 101)), btc_server_lockup_tx_id: Some(create_btc_history_txid("2222", 101)),
btc_claim_tx_id: Some(create_history_txid("3333", 102)), btc_claim_tx_id: Some(create_btc_history_txid("3333", 102)),
}; };
// When there's a lockup and confirmed claim tx, it should be Complete // When there's a lockup and confirmed claim tx, it should be Complete
@@ -31,10 +32,10 @@ mod test {
// Test with unconfirmed claim // Test with unconfirmed claim
let recovered_data = RecoveredOnchainDataChainSend { let recovered_data = RecoveredOnchainDataChainSend {
lbtc_user_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_user_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_refund_tx_id: None, lbtc_refund_tx_id: None,
btc_server_lockup_tx_id: Some(create_history_txid("2222", 101)), btc_server_lockup_tx_id: Some(create_btc_history_txid("2222", 101)),
btc_claim_tx_id: Some(create_history_txid("3333", 0)), // Unconfirmed claim btc_claim_tx_id: Some(create_btc_history_txid("3333", 0)), // Unconfirmed claim
}; };
// When there's a lockup and unconfirmed claim tx, it should be Pending // When there's a lockup and unconfirmed claim tx, it should be Pending
@@ -52,9 +53,9 @@ mod test {
fn test_derive_partial_state_with_lockup_and_refund() { fn test_derive_partial_state_with_lockup_and_refund() {
// Test with confirmed refund // Test with confirmed refund
let recovered_data = RecoveredOnchainDataChainSend { let recovered_data = RecoveredOnchainDataChainSend {
lbtc_user_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_user_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_refund_tx_id: Some(create_history_txid("4444", 102)), lbtc_refund_tx_id: Some(create_lbtc_history_txid("4444", 102)),
btc_server_lockup_tx_id: Some(create_history_txid("2222", 101)), btc_server_lockup_tx_id: Some(create_btc_history_txid("2222", 101)),
btc_claim_tx_id: None, btc_claim_tx_id: None,
}; };
@@ -70,9 +71,9 @@ mod test {
// Test with unconfirmed refund // Test with unconfirmed refund
let recovered_data = RecoveredOnchainDataChainSend { let recovered_data = RecoveredOnchainDataChainSend {
lbtc_user_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_user_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_refund_tx_id: Some(create_history_txid("4444", 0)), // Unconfirmed refund lbtc_refund_tx_id: Some(create_lbtc_history_txid("4444", 0)), // Unconfirmed refund
btc_server_lockup_tx_id: Some(create_history_txid("2222", 101)), btc_server_lockup_tx_id: Some(create_btc_history_txid("2222", 101)),
btc_claim_tx_id: None, btc_claim_tx_id: None,
}; };
@@ -90,7 +91,7 @@ mod test {
#[sdk_macros::test_all] #[sdk_macros::test_all]
fn test_derive_partial_state_with_lockup_only() { fn test_derive_partial_state_with_lockup_only() {
let recovered_data = RecoveredOnchainDataChainSend { let recovered_data = RecoveredOnchainDataChainSend {
lbtc_user_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_user_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_refund_tx_id: None, lbtc_refund_tx_id: None,
btc_server_lockup_tx_id: None, btc_server_lockup_tx_id: None,
btc_claim_tx_id: None, btc_claim_tx_id: None,
@@ -132,10 +133,10 @@ mod test {
fn test_derive_partial_state_with_lockup_claim_refund() { fn test_derive_partial_state_with_lockup_claim_refund() {
// This is an edge case where both claim and refund txs exist // This is an edge case where both claim and refund txs exist
let recovered_data = RecoveredOnchainDataChainSend { let recovered_data = RecoveredOnchainDataChainSend {
lbtc_user_lockup_tx_id: Some(create_history_txid("1111", 100)), lbtc_user_lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
lbtc_refund_tx_id: Some(create_history_txid("4444", 102)), lbtc_refund_tx_id: Some(create_lbtc_history_txid("4444", 102)),
btc_server_lockup_tx_id: Some(create_history_txid("2222", 101)), btc_server_lockup_tx_id: Some(create_btc_history_txid("2222", 101)),
btc_claim_tx_id: Some(create_history_txid("3333", 103)), btc_claim_tx_id: Some(create_btc_history_txid("3333", 103)),
}; };
// Complete state should take precedence over refund // Complete state should take precedence over refund

View File

@@ -1,23 +1,20 @@
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
bitcoin,
chain::liquid::MockLiquidChainService, chain::liquid::MockLiquidChainService,
model::{ChainSwap, PaymentState, SwapMetadata}, elements,
model::{BtcHistory, ChainSwap, LBtcHistory, PaymentState, SwapMetadata},
recover::{ recover::{
handlers::{tests::test::create_mock_lbtc_wallet_tx, ChainSendSwapHandler}, handlers::{tests::create_mock_lbtc_wallet_tx, ChainSendSwapHandler},
model::{HistoryTxId, RecoveryContext, TxMap}, model::{RecoveryContext, TxMap},
}, },
swapper::MockSwapper, swapper::MockSwapper,
}; };
use boltz_client::{ use bitcoin::OutPoint;
bitcoin::{self, OutPoint}, use bitcoin::{transaction::Version, ScriptBuf, Sequence};
Amount, LockTime, use boltz_client::{Amount, LockTime};
}; use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey;
use lwk_wollet::{
bitcoin::{transaction::Version, ScriptBuf, Sequence},
elements::{self},
elements_miniscript::slip77::MasterBlindingKey,
};
use std::{collections::HashMap, str::FromStr, sync::Arc}; use std::{collections::HashMap, str::FromStr, sync::Arc};
@@ -364,7 +361,7 @@ mod test {
let tx_id = elements::Txid::from_str(tx_id_hex).unwrap(); let tx_id = elements::Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };
@@ -400,7 +397,7 @@ mod test {
let tx_id = elements::Txid::from_str(tx_id_hex).unwrap(); let tx_id = elements::Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };
@@ -436,7 +433,7 @@ mod test {
let mut history = Vec::new(); let mut history = Vec::new();
for (tx_id_hex, height) in tx_ids { for (tx_id_hex, height) in tx_ids {
let tx_id = bitcoin::Txid::from_str(tx_id_hex).unwrap(); let tx_id = bitcoin::Txid::from_str(tx_id_hex).unwrap();
history.push(HistoryTxId { history.push(BtcHistory {
txid: tx_id.to_string().parse().unwrap(), txid: tx_id.to_string().parse().unwrap(),
height: *height as i32, height: *height as i32,
}); });

View File

@@ -3,7 +3,7 @@ mod test {
use crate::{ use crate::{
model::PaymentState, model::PaymentState,
recover::handlers::{ recover::handlers::{
handle_receive_swap::RecoveredOnchainDataReceive, tests::test::create_history_txid, handle_receive_swap::RecoveredOnchainDataReceive, tests::create_lbtc_history_txid,
}, },
}; };
@@ -14,8 +14,8 @@ mod test {
fn test_derive_partial_state_with_lockup_and_claim() { fn test_derive_partial_state_with_lockup_and_claim() {
// Test with confirmed claim // Test with confirmed claim
let recovered_data = RecoveredOnchainDataReceive { let recovered_data = RecoveredOnchainDataReceive {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: Some(create_history_txid("2222", 101)), // Confirmed claim claim_tx_id: Some(create_lbtc_history_txid("2222", 101)), // Confirmed claim
mrh_tx_id: None, mrh_tx_id: None,
mrh_amount_sat: None, mrh_amount_sat: None,
}; };
@@ -32,8 +32,8 @@ mod test {
// Test with unconfirmed claim // Test with unconfirmed claim
let recovered_data = RecoveredOnchainDataReceive { let recovered_data = RecoveredOnchainDataReceive {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: Some(create_history_txid("2222", 0)), // Unconfirmed claim claim_tx_id: Some(create_lbtc_history_txid("2222", 0)), // Unconfirmed claim
mrh_tx_id: None, mrh_tx_id: None,
mrh_amount_sat: None, mrh_amount_sat: None,
}; };
@@ -52,7 +52,7 @@ mod test {
#[sdk_macros::test_all] #[sdk_macros::test_all]
fn test_derive_partial_state_with_lockup_only() { fn test_derive_partial_state_with_lockup_only() {
let recovered_data = RecoveredOnchainDataReceive { let recovered_data = RecoveredOnchainDataReceive {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: None, claim_tx_id: None,
mrh_tx_id: None, mrh_tx_id: None,
mrh_amount_sat: None, mrh_amount_sat: None,
@@ -77,7 +77,7 @@ mod test {
let recovered_data = RecoveredOnchainDataReceive { let recovered_data = RecoveredOnchainDataReceive {
lockup_tx_id: None, lockup_tx_id: None,
claim_tx_id: None, claim_tx_id: None,
mrh_tx_id: Some(create_history_txid("3333", 103)), mrh_tx_id: Some(create_lbtc_history_txid("3333", 103)),
mrh_amount_sat: Some(100000), mrh_amount_sat: Some(100000),
}; };
@@ -95,7 +95,7 @@ mod test {
let recovered_data = RecoveredOnchainDataReceive { let recovered_data = RecoveredOnchainDataReceive {
lockup_tx_id: None, lockup_tx_id: None,
claim_tx_id: None, claim_tx_id: None,
mrh_tx_id: Some(create_history_txid("3333", 0)), // Unconfirmed MRH tx mrh_tx_id: Some(create_lbtc_history_txid("3333", 0)), // Unconfirmed MRH tx
mrh_amount_sat: Some(100000), mrh_amount_sat: Some(100000),
}; };

View File

@@ -2,17 +2,16 @@
mod test { mod test {
use crate::{ use crate::{
chain::liquid::MockLiquidChainService, chain::liquid::MockLiquidChainService,
model::{PaymentState, ReceiveSwap, SwapMetadata}, elements,
model::{LBtcHistory, PaymentState, ReceiveSwap, SwapMetadata},
recover::{ recover::{
handlers::{tests::test::create_mock_lbtc_wallet_tx, ReceiveSwapHandler}, handlers::{tests::create_mock_lbtc_wallet_tx, ReceiveSwapHandler},
model::{HistoryTxId, RecoveryContext, TxMap}, model::{RecoveryContext, TxMap},
}, },
swapper::MockSwapper, swapper::MockSwapper,
}; };
use boltz_client::ElementsAddress; use elements::{Address as ElementsAddress, Script, Txid};
use lwk_wollet::elements::{Script, Txid}; use lwk_wollet::{elements_miniscript::slip77::MasterBlindingKey, WalletTx};
use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey;
use lwk_wollet::WalletTx;
use std::{collections::HashMap, str::FromStr, sync::Arc}; use std::{collections::HashMap, str::FromStr, sync::Arc};
#[cfg(all(target_family = "wasm", target_os = "unknown"))] #[cfg(all(target_family = "wasm", target_os = "unknown"))]
@@ -324,7 +323,7 @@ mod test {
let tx_id = Txid::from_str(tx_id_hex).unwrap(); let tx_id = Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };
@@ -361,7 +360,7 @@ mod test {
let tx_id = Txid::from_str(tx_id_hex).unwrap(); let tx_id = Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };
@@ -391,7 +390,7 @@ mod test {
let tx_id = Txid::from_str(tx_id_hex).unwrap(); let tx_id = Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };

View File

@@ -3,7 +3,7 @@ mod test {
use crate::{ use crate::{
model::PaymentState, model::PaymentState,
recover::handlers::{ recover::handlers::{
handle_send_swap::RecoveredOnchainDataSend, tests::test::create_history_txid, handle_send_swap::RecoveredOnchainDataSend, tests::create_lbtc_history_txid,
}, },
}; };
@@ -13,8 +13,8 @@ mod test {
#[sdk_macros::test_all] #[sdk_macros::test_all]
fn test_derive_partial_state_with_lockup_and_claim() { fn test_derive_partial_state_with_lockup_and_claim() {
let recovered_data = RecoveredOnchainDataSend { let recovered_data = RecoveredOnchainDataSend {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: Some(create_history_txid("2222", 101)), claim_tx_id: Some(create_lbtc_history_txid("2222", 101)),
refund_tx_id: None, refund_tx_id: None,
preimage: None, preimage: None,
}; };
@@ -34,9 +34,9 @@ mod test {
fn test_derive_partial_state_with_lockup_and_refund() { fn test_derive_partial_state_with_lockup_and_refund() {
// Test with confirmed refund // Test with confirmed refund
let recovered_data = RecoveredOnchainDataSend { let recovered_data = RecoveredOnchainDataSend {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: None, claim_tx_id: None,
refund_tx_id: Some(create_history_txid("3333", 102)), refund_tx_id: Some(create_lbtc_history_txid("3333", 102)),
preimage: None, preimage: None,
}; };
@@ -52,9 +52,9 @@ mod test {
// Test with unconfirmed refund // Test with unconfirmed refund
let recovered_data = RecoveredOnchainDataSend { let recovered_data = RecoveredOnchainDataSend {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: None, claim_tx_id: None,
refund_tx_id: Some(create_history_txid("3333", 0)), // Unconfirmed tx refund_tx_id: Some(create_lbtc_history_txid("3333", 0)), // Unconfirmed tx
preimage: None, preimage: None,
}; };
@@ -72,7 +72,7 @@ mod test {
#[sdk_macros::test_all] #[sdk_macros::test_all]
fn test_derive_partial_state_with_lockup_only() { fn test_derive_partial_state_with_lockup_only() {
let recovered_data = RecoveredOnchainDataSend { let recovered_data = RecoveredOnchainDataSend {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: None, claim_tx_id: None,
refund_tx_id: None, refund_tx_id: None,
preimage: None, preimage: None,
@@ -114,9 +114,9 @@ mod test {
fn test_derive_partial_state_with_lockup_claim_refund() { fn test_derive_partial_state_with_lockup_claim_refund() {
// This is an edge case where both claim and refund txs exist // This is an edge case where both claim and refund txs exist
let recovered_data = RecoveredOnchainDataSend { let recovered_data = RecoveredOnchainDataSend {
lockup_tx_id: Some(create_history_txid("1111", 100)), lockup_tx_id: Some(create_lbtc_history_txid("1111", 100)),
claim_tx_id: Some(create_history_txid("2222", 101)), claim_tx_id: Some(create_lbtc_history_txid("2222", 101)),
refund_tx_id: Some(create_history_txid("3333", 102)), refund_tx_id: Some(create_lbtc_history_txid("3333", 102)),
preimage: None, preimage: None,
}; };

View File

@@ -2,7 +2,7 @@
mod test { mod test {
use crate::chain::liquid::MockLiquidChainService; use crate::chain::liquid::MockLiquidChainService;
use crate::prelude::*; use crate::prelude::*;
use crate::recover::handlers::tests::test::{ use crate::recover::handlers::tests::{
create_empty_lbtc_transaction, create_mock_lbtc_wallet_tx, create_empty_lbtc_transaction, create_mock_lbtc_wallet_tx,
}; };
use crate::recover::handlers::SendSwapHandler; use crate::recover::handlers::SendSwapHandler;
@@ -345,7 +345,7 @@ mod test {
let tx_id = Txid::from_str(tx_id_hex).unwrap(); let tx_id = Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };
@@ -380,7 +380,7 @@ mod test {
let tx_id = Txid::from_str(tx_id_hex).unwrap(); let tx_id = Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };
@@ -410,7 +410,7 @@ mod test {
let tx_id = Txid::from_str(tx_id_hex).unwrap(); let tx_id = Txid::from_str(tx_id_hex).unwrap();
// Create history tx // Create history tx
let history_tx = HistoryTxId { let history_tx = LBtcHistory {
txid: tx_id, txid: tx_id,
height: height as i32, height: height as i32,
}; };

View File

@@ -9,70 +9,73 @@ pub mod handle_receive_swap_tests_integration;
pub mod handle_send_swap_tests; pub mod handle_send_swap_tests;
pub mod handle_send_swap_tests_integration; pub mod handle_send_swap_tests_integration;
// Helper function to create a HistoryTxId for testing // Helper function to create a History txid for testing
mod test { use std::{collections::BTreeMap, str::FromStr};
use std::{collections::BTreeMap, str::FromStr};
use crate::recover::model::HistoryTxId; use crate::model::{BtcHistory, LBtcHistory};
use lwk_wollet::{ use crate::{bitcoin, elements};
elements::{self, AssetId, Transaction, TxIn, TxInWitness, Txid}, use elements::{AssetId, Transaction, TxIn, TxInWitness, Txid};
hashes::Hash, use lwk_wollet::{hashes::Hash, WalletTx};
WalletTx,
};
pub(crate) fn create_history_txid(hex_id: &str, height: i32) -> HistoryTxId { pub(crate) fn create_lbtc_history_txid(hex_id: &str, height: i32) -> LBtcHistory {
let txid_bytes = hex::decode(format!("{:0>64}", hex_id)).unwrap(); let txid_bytes = hex::decode(format!("{:0>64}", hex_id)).unwrap();
let mut txid_array = [0u8; 32]; let mut txid_array = [0u8; 32];
txid_array.copy_from_slice(&txid_bytes); txid_array.copy_from_slice(&txid_bytes);
HistoryTxId { LBtcHistory {
txid: Txid::from_slice(&txid_array).unwrap(), txid: elements::Txid::from_slice(&txid_array).unwrap(),
height, height,
} }
} }
// Create an empty LBTC transaction pub(crate) fn create_btc_history_txid(hex_id: &str, height: i32) -> BtcHistory {
pub(crate) fn create_empty_lbtc_transaction() -> Transaction { let txid_bytes = hex::decode(format!("{:0>64}", hex_id)).unwrap();
Transaction { let mut txid_array = [0u8; 32];
version: 2, txid_array.copy_from_slice(&txid_bytes);
lock_time: elements::LockTime::from_height(0).unwrap(),
input: vec![TxIn { BtcHistory {
previous_output: Default::default(), txid: bitcoin::Txid::from_slice(&txid_array).unwrap(),
is_pegin: false, height,
script_sig: elements::Script::new(), }
sequence: elements::Sequence::default(), }
asset_issuance: Default::default(),
witness: TxInWitness::empty(), // Create an empty LBTC transaction
}], pub(crate) fn create_empty_lbtc_transaction() -> Transaction {
output: vec![], Transaction {
} version: 2,
} lock_time: elements::LockTime::from_height(0).unwrap(),
input: vec![TxIn {
// Create a mock LBTC wallet transaction previous_output: Default::default(),
pub(crate) fn create_mock_lbtc_wallet_tx( is_pegin: false,
tx_id_hex: &str, script_sig: elements::Script::new(),
height: u32, sequence: elements::Sequence::default(),
amount: i64, asset_issuance: Default::default(),
) -> WalletTx { witness: TxInWitness::empty(),
let tx_id = Txid::from_str(tx_id_hex).unwrap(); }],
output: vec![],
WalletTx { }
txid: tx_id, }
tx: create_empty_lbtc_transaction(),
height: Some(height), // Create a mock LBTC wallet transaction
fee: 1000, pub(crate) fn create_mock_lbtc_wallet_tx(tx_id_hex: &str, height: u32, amount: i64) -> WalletTx {
timestamp: Some(1001), // Just after swap creation time let tx_id = Txid::from_str(tx_id_hex).unwrap();
balance: {
let mut map = BTreeMap::new(); WalletTx {
map.insert( txid: tx_id,
AssetId::from_slice(&[0; 32]).unwrap(), // Default asset ID tx: create_empty_lbtc_transaction(),
amount, height: Some(height),
); fee: 1000,
map timestamp: Some(1001), // Just after swap creation time
}, balance: {
outputs: vec![], let mut map = BTreeMap::new();
inputs: Vec::new(), map.insert(
type_: "".to_string(), AssetId::from_slice(&[0; 32]).unwrap(), // Default asset ID
} amount,
);
map
},
outputs: vec![],
inputs: Vec::new(),
type_: "".to_string(),
} }
} }

View File

@@ -2,59 +2,26 @@ use std::collections::HashMap;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use boltz_client::ElementsAddress;
use electrum_client::GetBalanceRes;
use lwk_wollet::elements::Txid;
use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey; use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey;
use lwk_wollet::History;
use lwk_wollet::WalletTx; use lwk_wollet::WalletTx;
use crate::chain::liquid::LiquidChainService; use crate::chain::liquid::LiquidChainService;
use crate::prelude::*; use crate::prelude::*;
use crate::swapper::Swapper; use crate::swapper::Swapper;
pub(crate) type BtcScript = lwk_wollet::bitcoin::ScriptBuf;
pub(crate) type LBtcScript = lwk_wollet::elements::Script;
#[derive(Clone, Debug)]
pub(crate) struct HistoryTxId {
pub txid: Txid,
/// Confirmation height of txid
///
/// -1 means unconfirmed with unconfirmed parents
/// 0 means unconfirmed with confirmed parents
pub height: i32,
}
impl HistoryTxId {
pub(crate) fn confirmed(&self) -> bool {
self.height > 0
}
}
impl From<History> for HistoryTxId {
fn from(value: History) -> Self {
Self::from(&value)
}
}
impl From<&History> for HistoryTxId {
fn from(value: &History) -> Self {
Self {
txid: value.txid,
height: value.height,
}
}
}
/// A map of all our known LWK onchain txs, indexed by tx ID. Essentially our own cache of the LWK txs. /// A map of all our known LWK onchain txs, indexed by tx ID. Essentially our own cache of the LWK txs.
pub(crate) struct TxMap { pub(crate) struct TxMap {
pub(crate) outgoing_tx_map: HashMap<Txid, WalletTx>, pub(crate) outgoing_tx_map: HashMap<elements::Txid, WalletTx>,
pub(crate) incoming_tx_map: HashMap<Txid, WalletTx>, pub(crate) incoming_tx_map: HashMap<elements::Txid, WalletTx>,
} }
impl TxMap { impl TxMap {
pub(crate) fn from_raw_tx_map(raw_tx_map: HashMap<Txid, WalletTx>) -> Self { pub(crate) fn from_raw_tx_map(raw_tx_map: HashMap<elements::Txid, WalletTx>) -> Self {
let (outgoing_tx_map, incoming_tx_map): (HashMap<Txid, WalletTx>, HashMap<Txid, WalletTx>) = let (outgoing_tx_map, incoming_tx_map): (
raw_tx_map HashMap<elements::Txid, WalletTx>,
.into_iter() HashMap<elements::Txid, WalletTx>,
.partition(|(_, tx)| tx.balance.values().sum::<i64>() < 0); ) = raw_tx_map
.into_iter()
.partition(|(_, tx)| tx.balance.values().sum::<i64>() < 0);
Self { Self {
outgoing_tx_map, outgoing_tx_map,
@@ -107,7 +74,8 @@ impl SwapsList {
} }
// Add MRH script if available // Add MRH script if available
if let Ok(mrh_address) = ElementsAddress::from_str(&receive_swap.mrh_address) { if let Ok(mrh_address) = elements::Address::from_str(&receive_swap.mrh_address)
{
swap_scripts.push(mrh_address.script_pubkey()); swap_scripts.push(mrh_address.script_pubkey());
} }
} }
@@ -174,10 +142,10 @@ impl SwapsList {
} }
pub(crate) struct RecoveryContext { pub(crate) struct RecoveryContext {
pub(crate) lbtc_script_to_history_map: HashMap<LBtcScript, Vec<HistoryTxId>>, pub(crate) lbtc_script_to_history_map: HashMap<LBtcScript, Vec<LBtcHistory>>,
pub(crate) btc_script_to_history_map: HashMap<BtcScript, Vec<HistoryTxId>>, pub(crate) btc_script_to_history_map: HashMap<BtcScript, Vec<BtcHistory>>,
pub(crate) btc_script_to_txs_map: HashMap<BtcScript, Vec<boltz_client::bitcoin::Transaction>>, pub(crate) btc_script_to_txs_map: HashMap<BtcScript, Vec<bitcoin::Transaction>>,
pub(crate) btc_script_to_balance_map: HashMap<BtcScript, GetBalanceRes>, pub(crate) btc_script_to_balance_map: HashMap<BtcScript, BtcScriptBalance>,
pub(crate) liquid_chain_service: Arc<dyn LiquidChainService>, pub(crate) liquid_chain_service: Arc<dyn LiquidChainService>,
pub(crate) swapper: Arc<dyn Swapper>, pub(crate) swapper: Arc<dyn Swapper>,
pub(crate) tx_map: TxMap, pub(crate) tx_map: TxMap,

View File

@@ -1,27 +1,29 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use anyhow::{anyhow, ensure, Result}; use anyhow::{anyhow, ensure, Result};
use electrum_client::GetBalanceRes;
use log::{debug, info, warn}; use log::{debug, info, warn};
use lwk_wollet::elements::Txid;
use lwk_wollet::elements_miniscript::slip77::MasterBlindingKey;
use lwk_wollet::hashes::hex::{DisplayHex, FromHex};
use lwk_wollet::WalletTx;
use super::handlers::{ use super::handlers::{
ChainReceiveSwapHandler, ChainSendSwapHandler, ReceiveSwapHandler, SendSwapHandler, ChainReceiveSwapHandler, ChainSendSwapHandler, ReceiveSwapHandler, SendSwapHandler,
}; };
use super::model::*; use super::model::*;
use crate::prelude::*;
use elements::Txid;
use lwk_wollet::{
elements_miniscript::slip77::MasterBlindingKey,
hashes::hex::{DisplayHex, FromHex},
WalletTx,
};
use crate::model::Direction;
use crate::persist::Persister;
use crate::prelude::Swap;
use crate::sdk::NETWORK_PROPAGATION_GRACE_PERIOD; use crate::sdk::NETWORK_PROPAGATION_GRACE_PERIOD;
use crate::swapper::Swapper; use crate::swapper::Swapper;
use crate::wallet::OnchainWallet; use crate::wallet::OnchainWallet;
use crate::{ use crate::{
chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService}, chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService},
recover::model::{BtcScript, HistoryTxId, LBtcScript}, model::{BtcScript, Direction, LBtcScript},
persist::Persister,
prelude::Swap,
utils, utils,
}; };
@@ -81,7 +83,7 @@ impl Recoverer {
let raw_tx_map = self.onchain_wallet.transactions_by_tx_id().await?; let raw_tx_map = self.onchain_wallet.transactions_by_tx_id().await?;
// Fetch chain tips for expiration checks // Fetch chain tips for expiration checks
let bitcoin_tip = self.bitcoin_chain_service.tip()?; let bitcoin_tip = self.bitcoin_chain_service.tip().await?;
let liquid_tip = self.liquid_chain_service.tip().await?; let liquid_tip = self.liquid_chain_service.tip().await?;
// Convert swaps to SwapsList and fetch history data // Convert swaps to SwapsList and fetch history data
@@ -91,7 +93,7 @@ impl Recoverer {
&swaps_list, &swaps_list,
TxMap::from_raw_tx_map(raw_tx_map.clone()), TxMap::from_raw_tx_map(raw_tx_map.clone()),
liquid_tip, liquid_tip,
bitcoin_tip.height as u32, bitcoin_tip,
self.master_blinding_key, self.master_blinding_key,
) )
.await?; .await?;
@@ -221,11 +223,11 @@ impl Recoverer {
async fn fetch_lbtc_history_map( async fn fetch_lbtc_history_map(
&self, &self,
swap_lbtc_scripts: Vec<LBtcScript>, swap_lbtc_scripts: Vec<LBtcScript>,
) -> Result<HashMap<LBtcScript, Vec<HistoryTxId>>> { ) -> Result<HashMap<LBtcScript, Vec<LBtcHistory>>> {
let t0 = web_time::Instant::now(); let t0 = web_time::Instant::now();
let lbtc_script_histories = self let lbtc_script_histories = self
.liquid_chain_service .liquid_chain_service
.get_scripts_history(&swap_lbtc_scripts.to_vec()) .get_scripts_history(&swap_lbtc_scripts)
.await?; .await?;
info!( info!(
"Recoverer executed liquid get_scripts_history for {} scripts in {} milliseconds", "Recoverer executed liquid get_scripts_history for {} scripts in {} milliseconds",
@@ -236,13 +238,12 @@ impl Recoverer {
let lbtc_swap_scripts_len = swap_lbtc_scripts.len(); let lbtc_swap_scripts_len = swap_lbtc_scripts.len();
let lbtc_script_histories_len = lbtc_script_histories.len(); let lbtc_script_histories_len = lbtc_script_histories.len();
ensure!( ensure!(
lbtc_swap_scripts_len == lbtc_script_histories_len, lbtc_swap_scripts_len == lbtc_script_histories_len,
anyhow!("Got {lbtc_script_histories_len} L-BTC script histories, expected {lbtc_swap_scripts_len}") anyhow!("Got {lbtc_script_histories_len} L-BTC script histories, expected {lbtc_swap_scripts_len}")
); );
let lbtc_script_to_history_map: HashMap<LBtcScript, Vec<HistoryTxId>> = swap_lbtc_scripts let lbtc_script_to_history_map: HashMap<LBtcScript, Vec<LBtcHistory>> = swap_lbtc_scripts
.into_iter() .into_iter()
.zip(lbtc_script_histories.into_iter()) .zip(lbtc_script_histories.into_iter())
.map(|(k, v)| (k, v.into_iter().map(HistoryTxId::from).collect()))
.collect(); .collect();
Ok(lbtc_script_to_history_map) Ok(lbtc_script_to_history_map)
@@ -252,9 +253,9 @@ impl Recoverer {
&self, &self,
swap_btc_script_bufs: Vec<BtcScript>, swap_btc_script_bufs: Vec<BtcScript>,
) -> Result<( ) -> Result<(
HashMap<BtcScript, Vec<HistoryTxId>>, HashMap<BtcScript, Vec<BtcHistory>>,
HashMap<BtcScript, Vec<boltz_client::bitcoin::Transaction>>, HashMap<BtcScript, Vec<bitcoin::Transaction>>,
HashMap<BtcScript, GetBalanceRes>, HashMap<BtcScript, BtcScriptBalance>,
)> { )> {
let swap_btc_scripts = swap_btc_script_bufs let swap_btc_scripts = swap_btc_script_bufs
.iter() .iter()
@@ -264,7 +265,8 @@ impl Recoverer {
let t0 = web_time::Instant::now(); let t0 = web_time::Instant::now();
let btc_script_histories = self let btc_script_histories = self
.bitcoin_chain_service .bitcoin_chain_service
.get_scripts_history(&swap_btc_scripts)?; .get_scripts_history(&swap_btc_scripts)
.await?;
info!( info!(
"Recoverer executed bitcoin get_scripts_history for {} scripts in {} milliseconds", "Recoverer executed bitcoin get_scripts_history for {} scripts in {} milliseconds",
@@ -285,17 +287,17 @@ impl Recoverer {
btc_swap_scripts_len == btc_script_histories_len, btc_swap_scripts_len == btc_script_histories_len,
anyhow!("Got {btc_script_histories_len} BTC script histories, expected {btc_swap_scripts_len}") anyhow!("Got {btc_script_histories_len} BTC script histories, expected {btc_swap_scripts_len}")
); );
let btc_script_to_history_map: HashMap<BtcScript, Vec<HistoryTxId>> = swap_btc_script_bufs let btc_script_to_history_map: HashMap<BtcScript, Vec<BtcHistory>> = swap_btc_script_bufs
.clone() .clone()
.into_iter() .into_iter()
.zip(btc_script_histories.iter()) .zip(btc_script_histories.clone())
.map(|(k, v)| (k, v.iter().map(HistoryTxId::from).collect()))
.collect(); .collect();
let t0 = web_time::Instant::now(); let t0 = web_time::Instant::now();
let btc_script_txs = self let btc_script_txs = self
.bitcoin_chain_service .bitcoin_chain_service
.get_transactions(&btx_script_tx_ids)?; .get_transactions(&btx_script_tx_ids)
.await?;
info!( info!(
"Recoverer executed bitcoin get_transactions for {} transactions in {} milliseconds", "Recoverer executed bitcoin get_transactions for {} transactions in {} milliseconds",
btx_script_tx_ids.len(), btx_script_tx_ids.len(),
@@ -305,7 +307,8 @@ impl Recoverer {
let t0 = web_time::Instant::now(); let t0 = web_time::Instant::now();
let btc_script_balances = self let btc_script_balances = self
.bitcoin_chain_service .bitcoin_chain_service
.scripts_get_balance(&swap_btc_scripts)?; .scripts_get_balance(&swap_btc_scripts)
.await?;
info!( info!(
"Recoverer executed bitcoin scripts_get_balance for {} scripts in {} milliseconds", "Recoverer executed bitcoin scripts_get_balance for {} scripts in {} milliseconds",
swap_btc_scripts.len(), swap_btc_scripts.len(),
@@ -318,8 +321,9 @@ impl Recoverer {
.into_iter() .into_iter()
.zip(btc_script_histories.iter()) .zip(btc_script_histories.iter())
.map(|(script, history)| { .map(|(script, history)| {
let relevant_tx_ids: Vec<Txid> = history.iter().map(|h| h.txid).collect(); let relevant_tx_ids: Vec<bitcoin::Txid> =
let relevant_txs: Vec<boltz_client::bitcoin::Transaction> = btc_script_txs history.iter().map(|h| h.txid).collect();
let relevant_txs: Vec<bitcoin::Transaction> = btc_script_txs
.iter() .iter()
.filter(|&tx| { .filter(|&tx| {
relevant_tx_ids.contains(&tx.compute_txid().to_raw_hash().into()) relevant_tx_ids.contains(&tx.compute_txid().to_raw_hash().into())
@@ -331,7 +335,7 @@ impl Recoverer {
}) })
.collect(); .collect();
let btc_script_to_balance_map: HashMap<BtcScript, GetBalanceRes> = swap_btc_script_bufs let btc_script_to_balance_map: HashMap<BtcScript, BtcScriptBalance> = swap_btc_script_bufs
.into_iter() .into_iter()
.zip(btc_script_balances) .zip(btc_script_balances)
.collect(); .collect();

View File

@@ -5,8 +5,7 @@ use std::{path::PathBuf, str::FromStr, sync::Arc, time::Duration};
use anyhow::{anyhow, ensure, Result}; use anyhow::{anyhow, ensure, Result};
use boltz_client::{swaps::boltz::*, util::secrets::Preimage}; use boltz_client::{swaps::boltz::*, util::secrets::Preimage};
use buy::{BuyBitcoinApi, BuyBitcoinService}; use buy::{BuyBitcoinApi, BuyBitcoinService};
use chain::bitcoin::HybridBitcoinChainService; use chain::{bitcoin::BitcoinChainService, liquid::LiquidChainService};
use chain::liquid::{HybridLiquidChainService, LiquidChainService};
use chain_swap::ESTIMATED_BTC_CLAIM_TX_VSIZE; use chain_swap::ESTIMATED_BTC_CLAIM_TX_VSIZE;
use futures_util::stream::select_all; use futures_util::stream::select_all;
use futures_util::{StreamExt, TryFutureExt}; use futures_util::{StreamExt, TryFutureExt};
@@ -31,7 +30,6 @@ use tokio_with_wasm::alias as tokio;
use web_time::Instant; use web_time::Instant;
use x509_parser::parse_x509_certificate; use x509_parser::parse_x509_certificate;
use crate::chain::bitcoin::BitcoinChainService;
use crate::chain_swap::ChainSwapHandler; use crate::chain_swap::ChainSwapHandler;
use crate::ensure_sdk; use crate::ensure_sdk;
use crate::error::SdkError; use crate::error::SdkError;
@@ -208,16 +206,13 @@ impl LiquidSdkBuilder {
let bitcoin_chain_service: Arc<dyn BitcoinChainService> = let bitcoin_chain_service: Arc<dyn BitcoinChainService> =
match self.bitcoin_chain_service.clone() { match self.bitcoin_chain_service.clone() {
Some(bitcoin_chain_service) => bitcoin_chain_service, Some(bitcoin_chain_service) => bitcoin_chain_service,
None => Arc::new(HybridBitcoinChainService::new( None => self.config.bitcoin_chain_service(),
self.config.clone(),
rest_client.clone(),
)?),
}; };
let liquid_chain_service: Arc<dyn LiquidChainService> = let liquid_chain_service: Arc<dyn LiquidChainService> =
match self.liquid_chain_service.clone() { match self.liquid_chain_service.clone() {
Some(liquid_chain_service) => liquid_chain_service, Some(liquid_chain_service) => liquid_chain_service,
None => Arc::new(HybridLiquidChainService::new(self.config.clone())?), None => self.config.liquid_chain_service(),
}; };
let onchain_wallet: Arc<dyn OnchainWallet> = match self.onchain_wallet.clone() { let onchain_wallet: Arc<dyn OnchainWallet> = match self.onchain_wallet.clone() {
@@ -580,7 +575,7 @@ impl LiquidSdk {
}; };
// Get the Bitcoin tip and process a new block // Get the Bitcoin tip and process a new block
let t0 = Instant::now(); let t0 = Instant::now();
let bitcoin_tip_res = cloned.bitcoin_chain_service.tip().map(|tip| tip.height as u32); let bitcoin_tip_res = cloned.bitcoin_chain_service.tip().await;
let duration_ms = Instant::now().duration_since(t0).as_millis(); let duration_ms = Instant::now().duration_since(t0).as_millis();
info!("Fetched bitcoin tip at ({duration_ms} ms)"); info!("Fetched bitcoin tip at ({duration_ms} ms)");
let is_new_bitcoin_block = match &bitcoin_tip_res { let is_new_bitcoin_block = match &bitcoin_tip_res {
@@ -2684,7 +2679,8 @@ impl LiquidSdk {
.collect(); .collect();
let scripts_utxos = self let scripts_utxos = self
.bitcoin_chain_service .bitcoin_chain_service
.get_scripts_utxos(&lockup_scripts)?; .get_scripts_utxos(&lockup_scripts)
.await?;
let mut refundables = vec![]; let mut refundables = vec![];
for (chain_swap, script_utxos) in chain_swaps.into_iter().zip(scripts_utxos) { for (chain_swap, script_utxos) in chain_swaps.into_iter().zip(scripts_utxos) {
@@ -2900,7 +2896,7 @@ impl LiquidSdk {
.collect(); .collect();
match partial_sync { match partial_sync {
false => { false => {
let bitcoin_height = self.bitcoin_chain_service.tip()?.height as u32; let bitcoin_height = self.bitcoin_chain_service.tip().await?;
let liquid_height = self.liquid_chain_service.tip().await?; let liquid_height = self.liquid_chain_service.tip().await?;
let final_swap_states = [PaymentState::Complete, PaymentState::Failed]; let final_swap_states = [PaymentState::Complete, PaymentState::Failed];
@@ -3859,7 +3855,7 @@ mod tests {
boltz::{self, TransactionInfo}, boltz::{self, TransactionInfo},
swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates}, swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates},
}; };
use lwk_wollet::{elements::Txid, hashes::hex::DisplayHex}; use lwk_wollet::hashes::hex::DisplayHex as _;
use crate::chain_swap::ESTIMATED_BTC_LOCKUP_TX_VSIZE; use crate::chain_swap::ESTIMATED_BTC_LOCKUP_TX_VSIZE;
use crate::test_utils::chain_swap::{ use crate::test_utils::chain_swap::{
@@ -3869,10 +3865,11 @@ mod tests {
use crate::test_utils::swapper::ZeroAmountSwapMockConfig; use crate::test_utils::swapper::ZeroAmountSwapMockConfig;
use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX; use crate::test_utils::wallet::TEST_LIQUID_RECEIVE_LOCKUP_TX;
use crate::{ use crate::{
model::{Direction, PaymentState, Swap}, bitcoin, elements,
model::{BtcHistory, Direction, LBtcHistory, PaymentState, Swap},
sdk::LiquidSdk, sdk::LiquidSdk,
test_utils::{ test_utils::{
chain::{MockBitcoinChainService, MockHistory, MockLiquidChainService}, chain::{MockBitcoinChainService, MockLiquidChainService},
chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX}, chain_swap::{new_chain_swap, TEST_BITCOIN_INCOMING_USER_LOCKUP_TX},
persist::{create_persister, new_receive_swap, new_send_swap}, persist::{create_persister, new_receive_swap, new_send_swap},
sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services}, sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services},
@@ -4059,11 +4056,9 @@ mod tests {
let height = (serde_json::to_string(&status).unwrap() let height = (serde_json::to_string(&status).unwrap()
== serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap()) == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
as i32; as i32;
liquid_chain_service.set_history(vec![MockHistory { liquid_chain_service.set_history(vec![LBtcHistory {
txid: mock_tx_id, txid: mock_tx_id,
height, height,
block_hash: None,
block_timestamp: None,
}]); }]);
let persisted_swap = trigger_swap_update!( let persisted_swap = trigger_swap_update!(
@@ -4095,11 +4090,9 @@ mod tests {
let height = (serde_json::to_string(&status).unwrap() let height = (serde_json::to_string(&status).unwrap()
== serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap()) == serde_json::to_string(&RevSwapStates::TransactionConfirmed).unwrap())
as i32; as i32;
liquid_chain_service.set_history(vec![MockHistory { liquid_chain_service.set_history(vec![LBtcHistory {
txid: mock_tx_id, txid: mock_tx_id,
height, height,
block_hash: None,
block_timestamp: None,
}]); }]);
let persisted_swap = trigger_swap_update!( let persisted_swap = trigger_swap_update!(
@@ -4266,19 +4259,15 @@ mod tests {
if let Some(user_lockup_tx_id) = user_lockup_tx_id { if let Some(user_lockup_tx_id) = user_lockup_tx_id {
match direction { match direction {
Direction::Incoming => { Direction::Incoming => {
bitcoin_chain_service.set_history(vec![MockHistory { bitcoin_chain_service.set_history(vec![BtcHistory {
txid: Txid::from_str(user_lockup_tx_id).unwrap(), txid: bitcoin::Txid::from_str(user_lockup_tx_id).unwrap(),
height: 0, height: 0,
block_hash: None,
block_timestamp: None,
}]); }]);
} }
Direction::Outgoing => { Direction::Outgoing => {
liquid_chain_service.set_history(vec![MockHistory { liquid_chain_service.set_history(vec![LBtcHistory {
txid: Txid::from_str(user_lockup_tx_id).unwrap(), txid: elements::Txid::from_str(user_lockup_tx_id).unwrap(),
height: 0, height: 0,
block_hash: None,
block_timestamp: None,
}]); }]);
} }
} }
@@ -4313,11 +4302,9 @@ mod tests {
ChainSwapStates::TransactionConfirmed, ChainSwapStates::TransactionConfirmed,
] { ] {
if direction == Direction::Incoming { if direction == Direction::Incoming {
bitcoin_chain_service.set_history(vec![MockHistory { bitcoin_chain_service.set_history(vec![BtcHistory {
txid: Txid::from_str(&mock_user_lockup_tx_id).unwrap(), txid: bitcoin::Txid::from_str(&mock_user_lockup_tx_id).unwrap(),
height: 0, height: 0,
block_hash: None,
block_timestamp: None,
}]); }]);
bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]); bitcoin_chain_service.set_transactions(&[&mock_user_lockup_tx_hex]);
} }

View File

@@ -59,14 +59,14 @@ impl<P: ProxyUrlFetcher> BoltzSwapper<P> {
config: config.clone(), config: config.clone(),
liquid_electrum_client: ElectrumLiquidClient::new( liquid_electrum_client: ElectrumLiquidClient::new(
config.network.into(), config.network.into(),
&config.liquid_electrum_url, config.liquid_explorer.url(),
tls, tls,
validate_domain, validate_domain,
100, 100,
)?, )?,
bitcoin_electrum_client: ElectrumBitcoinClient::new( bitcoin_electrum_client: ElectrumBitcoinClient::new(
config.network.as_bitcoin_chain(), config.network.as_bitcoin_chain(),
&config.bitcoin_electrum_url, config.bitcoin_explorer.url(),
tls, tls,
validate_domain, validate_domain,
100, 100,

View File

@@ -2,23 +2,15 @@
use std::sync::Mutex; use std::sync::Mutex;
use crate::{
bitcoin, elements,
model::{BtcHistory, BtcScriptBalance, LBtcHistory},
};
use anyhow::Result; use anyhow::Result;
use boltz_client::{ use bitcoin::{consensus::deserialize, OutPoint, Script, TxOut};
elements::{ use boltz_client::Amount;
hex::FromHex, OutPoint as ElementsOutPoint, Script as ElementsScript, use elements::{
TxOut as ElementsTxOut, hex::FromHex, OutPoint as ElementsOutPoint, Script as ElementsScript, TxOut as ElementsTxOut,
},
Amount,
};
use electrum_client::GetBalanceRes;
use electrum_client::{
bitcoin::{consensus::deserialize, OutPoint, Script, TxOut},
HeaderNotification,
};
use lwk_wollet::{
bitcoin::constants::genesis_block,
elements::{BlockHash, Txid as ElementsTxid},
History,
}; };
use crate::{ use crate::{
@@ -27,28 +19,9 @@ use crate::{
utils, utils,
}; };
#[derive(Clone)]
pub(crate) struct MockHistory {
pub txid: ElementsTxid,
pub height: i32,
pub block_hash: Option<BlockHash>,
pub block_timestamp: Option<u32>,
}
impl From<MockHistory> for lwk_wollet::History {
fn from(h: MockHistory) -> Self {
lwk_wollet::History {
txid: h.txid,
height: h.height,
block_hash: h.block_hash,
block_timestamp: h.block_timestamp,
}
}
}
#[derive(Default)] #[derive(Default)]
pub(crate) struct MockLiquidChainService { pub(crate) struct MockLiquidChainService {
history: Mutex<Vec<MockHistory>>, history: Mutex<Vec<LBtcHistory>>,
} }
impl MockLiquidChainService { impl MockLiquidChainService {
@@ -56,12 +29,12 @@ impl MockLiquidChainService {
MockLiquidChainService::default() MockLiquidChainService::default()
} }
pub(crate) fn set_history(&self, history: Vec<MockHistory>) -> &Self { pub(crate) fn set_history(&self, history: Vec<LBtcHistory>) -> &Self {
*self.history.lock().unwrap() = history; *self.history.lock().unwrap() = history;
self self
} }
pub(crate) fn get_history(&self) -> Vec<MockHistory> { pub(crate) fn get_history(&self) -> Vec<LBtcHistory> {
self.history.lock().unwrap().clone() self.history.lock().unwrap().clone()
} }
} }
@@ -72,43 +45,40 @@ impl LiquidChainService for MockLiquidChainService {
Ok(0) Ok(0)
} }
async fn broadcast( async fn broadcast(&self, tx: &elements::Transaction) -> Result<elements::Txid> {
&self,
tx: &lwk_wollet::elements::Transaction,
) -> Result<lwk_wollet::elements::Txid> {
Ok(tx.txid()) Ok(tx.txid())
} }
async fn get_transaction_hex( async fn get_transaction_hex(
&self, &self,
_txid: &lwk_wollet::elements::Txid, _txid: &elements::Txid,
) -> Result<Option<lwk_wollet::elements::Transaction>> { ) -> Result<Option<elements::Transaction>> {
unimplemented!() unimplemented!()
} }
async fn get_transactions( async fn get_transactions(
&self, &self,
_txids: &[lwk_wollet::elements::Txid], _txids: &[elements::Txid],
) -> Result<Vec<lwk_wollet::elements::Transaction>> { ) -> Result<Vec<elements::Transaction>> {
Ok(vec![]) Ok(vec![])
} }
async fn get_script_history(
&self,
_scripts: &ElementsScript,
) -> Result<Vec<lwk_wollet::History>> {
Ok(self.get_history().into_iter().map(Into::into).collect())
}
async fn get_script_history_with_retry( async fn get_script_history_with_retry(
&self, &self,
_script: &ElementsScript, _script: &ElementsScript,
_retries: u64, _retries: u64,
) -> Result<Vec<lwk_wollet::History>> { ) -> Result<Vec<LBtcHistory>> {
Ok(self.get_history().into_iter().map(Into::into).collect()) Ok(self.get_history().into_iter().map(Into::into).collect())
} }
async fn get_scripts_history(&self, _scripts: &[ElementsScript]) -> Result<Vec<Vec<History>>> { async fn get_script_history(&self, _script: &ElementsScript) -> Result<Vec<LBtcHistory>> {
Ok(vec![])
}
async fn get_scripts_history(
&self,
_scripts: &[ElementsScript],
) -> Result<Vec<Vec<LBtcHistory>>> {
Ok(vec![]) Ok(vec![])
} }
@@ -121,18 +91,18 @@ impl LiquidChainService for MockLiquidChainService {
async fn verify_tx( async fn verify_tx(
&self, &self,
_address: &boltz_client::ElementsAddress, _address: &elements::Address,
_tx_id: &str, _tx_id: &str,
tx_hex: &str, tx_hex: &str,
_verify_confirmation: bool, _verify_confirmation: bool,
) -> Result<lwk_wollet::elements::Transaction> { ) -> Result<elements::Transaction> {
utils::deserialize_tx_hex(tx_hex) utils::deserialize_tx_hex(tx_hex)
} }
} }
pub(crate) struct MockBitcoinChainService { pub(crate) struct MockBitcoinChainService {
history: Mutex<Vec<MockHistory>>, history: Mutex<Vec<BtcHistory>>,
txs: Mutex<Vec<boltz_client::bitcoin::Transaction>>, txs: Mutex<Vec<bitcoin::Transaction>>,
script_balance_sat: Mutex<u64>, script_balance_sat: Mutex<u64>,
} }
@@ -145,7 +115,7 @@ impl MockBitcoinChainService {
} }
} }
pub(crate) fn set_history(&self, history: Vec<MockHistory>) -> &Self { pub(crate) fn set_history(&self, history: Vec<BtcHistory>) -> &Self {
*self.history.lock().unwrap() = history; *self.history.lock().unwrap() = history;
self self
} }
@@ -166,43 +136,26 @@ impl MockBitcoinChainService {
#[sdk_macros::async_trait] #[sdk_macros::async_trait]
impl BitcoinChainService for MockBitcoinChainService { impl BitcoinChainService for MockBitcoinChainService {
fn tip(&self) -> Result<HeaderNotification> { async fn tip(&self) -> Result<u32> {
Ok(HeaderNotification { Ok(0)
height: 0,
header: genesis_block(lwk_wollet::bitcoin::Network::Testnet).header,
})
} }
fn broadcast( async fn broadcast(&self, tx: &bitcoin::Transaction) -> Result<bitcoin::Txid, anyhow::Error> {
&self,
tx: &boltz_client::bitcoin::Transaction,
) -> Result<boltz_client::bitcoin::Txid, anyhow::Error> {
Ok(tx.compute_txid()) Ok(tx.compute_txid())
} }
fn get_transactions( async fn get_transactions(
&self, &self,
_txids: &[boltz_client::bitcoin::Txid], _txids: &[bitcoin::Txid],
) -> Result<Vec<boltz_client::bitcoin::Transaction>> { ) -> Result<Vec<bitcoin::Transaction>> {
Ok(self.txs.lock().unwrap().clone()) Ok(self.txs.lock().unwrap().clone())
} }
fn get_script_history(&self, _script: &Script) -> Result<Vec<lwk_wollet::History>> {
Ok(self
.history
.lock()
.unwrap()
.clone()
.into_iter()
.map(Into::into)
.collect())
}
async fn get_script_history_with_retry( async fn get_script_history_with_retry(
&self, &self,
_script: &Script, _script: &Script,
_retries: u64, _retries: u64,
) -> Result<Vec<lwk_wollet::History>> { ) -> Result<Vec<BtcHistory>> {
Ok(self Ok(self
.history .history
.lock() .lock()
@@ -213,19 +166,24 @@ impl BitcoinChainService for MockBitcoinChainService {
.collect()) .collect())
} }
fn get_scripts_history(&self, _scripts: &[&Script]) -> Result<Vec<Vec<History>>> { async fn get_script_history(&self, _scripts: &Script) -> Result<Vec<BtcHistory>> {
Ok(vec![]) Ok(vec![])
} }
fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> { async fn get_scripts_history(&self, _scripts: &[&Script]) -> Result<Vec<Vec<BtcHistory>>> {
Ok(vec![])
}
async fn get_script_utxos(&self, script: &Script) -> Result<Vec<Utxo>> {
Ok(self Ok(self
.get_scripts_utxos(&[script])? .get_scripts_utxos(&[script])
.await?
.first() .first()
.cloned() .cloned()
.unwrap_or_default()) .unwrap_or_default())
} }
fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>> { async fn get_scripts_utxos(&self, scripts: &[&Script]) -> Result<Vec<Vec<Utxo>>> {
let scripts_utxos = scripts let scripts_utxos = scripts
.iter() .iter()
.map(|s| { .map(|s| {
@@ -241,17 +199,17 @@ impl BitcoinChainService for MockBitcoinChainService {
Ok(scripts_utxos) Ok(scripts_utxos)
} }
fn script_get_balance( async fn script_get_balance(
&self, &self,
_script: &boltz_client::bitcoin::Script, _script: &boltz_client::bitcoin::Script,
) -> Result<electrum_client::GetBalanceRes> { ) -> Result<BtcScriptBalance> {
Ok(GetBalanceRes { Ok(BtcScriptBalance {
confirmed: 0, confirmed: 0,
unconfirmed: 0, unconfirmed: 0,
}) })
} }
fn scripts_get_balance(&self, _scripts: &[&Script]) -> Result<Vec<GetBalanceRes>> { async fn scripts_get_balance(&self, _scripts: &[&Script]) -> Result<Vec<BtcScriptBalance>> {
Ok(vec![]) Ok(vec![])
} }
@@ -259,8 +217,8 @@ impl BitcoinChainService for MockBitcoinChainService {
&self, &self,
_script: &boltz_client::bitcoin::Script, _script: &boltz_client::bitcoin::Script,
_retries: u64, _retries: u64,
) -> Result<electrum_client::GetBalanceRes> { ) -> Result<BtcScriptBalance> {
Ok(GetBalanceRes { Ok(BtcScriptBalance {
confirmed: *self.script_balance_sat.lock().unwrap(), confirmed: *self.script_balance_sat.lock().unwrap(),
unconfirmed: 0, unconfirmed: 0,
}) })

View File

@@ -398,7 +398,7 @@ impl OnchainWallet for LiquidOnchainWallet {
}; };
let electrum_url = let electrum_url =
ElectrumUrl::new(&self.config.liquid_electrum_url, tls, validate_domain)?; ElectrumUrl::new(self.config.liquid_explorer.url(), tls, validate_domain)?;
*electrum_client = Some(ElectrumClient::with_options( *electrum_client = Some(ElectrumClient::with_options(
&electrum_url, &electrum_url,
ElectrumOptions { timeout: Some(3) }, ElectrumOptions { timeout: Some(3) },

View File

@@ -293,12 +293,17 @@ pub struct Symbol {
pub position: Option<u32>, pub position: Option<u32>,
} }
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::BlockchainExplorer)]
pub enum BlockchainExplorer {
Electrum { url: String },
Esplora { url: String, use_waterfalls: bool },
}
#[derive(Clone)] #[derive(Clone)]
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::Config)] #[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::Config)]
pub struct Config { pub struct Config {
pub liquid_electrum_url: String, pub liquid_explorer: BlockchainExplorer,
pub bitcoin_electrum_url: String, pub bitcoin_explorer: BlockchainExplorer,
pub mempoolspace_url: String,
pub working_dir: String, pub working_dir: String,
pub cache_dir: Option<String>, pub cache_dir: Option<String>,
pub network: LiquidNetwork, pub network: LiquidNetwork,

View File

@@ -1527,6 +1527,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
} }
@protected
BlockchainExplorer dco_decode_blockchain_explorer(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
switch (raw[0]) {
case 0:
return BlockchainExplorer_Electrum(url: dco_decode_String(raw[1]));
case 1:
return BlockchainExplorer_Esplora(
url: dco_decode_String(raw[1]),
useWaterfalls: dco_decode_bool(raw[2]),
);
default:
throw Exception("unreachable");
}
}
@protected @protected
BlockchainInfo dco_decode_blockchain_info(dynamic raw) { BlockchainInfo dco_decode_blockchain_info(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1912,22 +1928,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
Config dco_decode_config(dynamic raw) { Config dco_decode_config(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 != 14) throw Exception('unexpected arr length: expect 14 but see ${arr.length}'); if (arr.length != 13) throw Exception('unexpected arr length: expect 13 but see ${arr.length}');
return Config( return Config(
liquidElectrumUrl: dco_decode_String(arr[0]), liquidExplorer: dco_decode_blockchain_explorer(arr[0]),
bitcoinElectrumUrl: dco_decode_String(arr[1]), bitcoinExplorer: dco_decode_blockchain_explorer(arr[1]),
mempoolspaceUrl: dco_decode_String(arr[2]), workingDir: dco_decode_String(arr[2]),
workingDir: dco_decode_String(arr[3]), cacheDir: dco_decode_opt_String(arr[3]),
cacheDir: dco_decode_opt_String(arr[4]), network: dco_decode_liquid_network(arr[4]),
network: dco_decode_liquid_network(arr[5]), paymentTimeoutSec: dco_decode_u_64(arr[5]),
paymentTimeoutSec: dco_decode_u_64(arr[6]), syncServiceUrl: dco_decode_opt_String(arr[6]),
syncServiceUrl: dco_decode_opt_String(arr[7]), zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[7]),
zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[8]), breezApiKey: dco_decode_opt_String(arr[8]),
breezApiKey: dco_decode_opt_String(arr[9]), externalInputParsers: dco_decode_opt_list_external_input_parser(arr[9]),
externalInputParsers: dco_decode_opt_list_external_input_parser(arr[10]), useDefaultExternalInputParsers: dco_decode_bool(arr[10]),
useDefaultExternalInputParsers: dco_decode_bool(arr[11]), onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[11]),
onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[12]), assetMetadata: dco_decode_opt_list_asset_metadata(arr[12]),
assetMetadata: dco_decode_opt_list_asset_metadata(arr[13]),
); );
} }
@@ -3532,6 +3547,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
); );
} }
@protected
BlockchainExplorer sse_decode_blockchain_explorer(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var tag_ = sse_decode_i_32(deserializer);
switch (tag_) {
case 0:
var var_url = sse_decode_String(deserializer);
return BlockchainExplorer_Electrum(url: var_url);
case 1:
var var_url = sse_decode_String(deserializer);
var var_useWaterfalls = sse_decode_bool(deserializer);
return BlockchainExplorer_Esplora(url: var_url, useWaterfalls: var_useWaterfalls);
default:
throw UnimplementedError('');
}
}
@protected @protected
BlockchainInfo sse_decode_blockchain_info(SseDeserializer deserializer) { BlockchainInfo sse_decode_blockchain_info(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -3918,9 +3951,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected @protected
Config sse_decode_config(SseDeserializer deserializer) { Config sse_decode_config(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
var var_liquidElectrumUrl = sse_decode_String(deserializer); var var_liquidExplorer = sse_decode_blockchain_explorer(deserializer);
var var_bitcoinElectrumUrl = sse_decode_String(deserializer); var var_bitcoinExplorer = sse_decode_blockchain_explorer(deserializer);
var var_mempoolspaceUrl = sse_decode_String(deserializer);
var var_workingDir = sse_decode_String(deserializer); var var_workingDir = sse_decode_String(deserializer);
var var_cacheDir = sse_decode_opt_String(deserializer); var var_cacheDir = sse_decode_opt_String(deserializer);
var var_network = sse_decode_liquid_network(deserializer); var var_network = sse_decode_liquid_network(deserializer);
@@ -3933,9 +3965,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
var var_onchainFeeRateLeewaySatPerVbyte = sse_decode_opt_box_autoadd_u_32(deserializer); var var_onchainFeeRateLeewaySatPerVbyte = sse_decode_opt_box_autoadd_u_32(deserializer);
var var_assetMetadata = sse_decode_opt_list_asset_metadata(deserializer); var var_assetMetadata = sse_decode_opt_list_asset_metadata(deserializer);
return Config( return Config(
liquidElectrumUrl: var_liquidElectrumUrl, liquidExplorer: var_liquidExplorer,
bitcoinElectrumUrl: var_bitcoinElectrumUrl, bitcoinExplorer: var_bitcoinExplorer,
mempoolspaceUrl: var_mempoolspaceUrl,
workingDir: var_workingDir, workingDir: var_workingDir,
cacheDir: var_cacheDir, cacheDir: var_cacheDir,
network: var_network, network: var_network,
@@ -5992,6 +6023,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_String(self.message, serializer); sse_encode_opt_String(self.message, serializer);
} }
@protected
void sse_encode_blockchain_explorer(BlockchainExplorer self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
switch (self) {
case BlockchainExplorer_Electrum(url: final url):
sse_encode_i_32(0, serializer);
sse_encode_String(url, serializer);
case BlockchainExplorer_Esplora(url: final url, useWaterfalls: final useWaterfalls):
sse_encode_i_32(1, serializer);
sse_encode_String(url, serializer);
sse_encode_bool(useWaterfalls, serializer);
}
}
@protected @protected
void sse_encode_blockchain_info(BlockchainInfo self, SseSerializer serializer) { void sse_encode_blockchain_info(BlockchainInfo self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -6398,9 +6443,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected @protected
void sse_encode_config(Config self, SseSerializer serializer) { void sse_encode_config(Config 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_String(self.liquidElectrumUrl, serializer); sse_encode_blockchain_explorer(self.liquidExplorer, serializer);
sse_encode_String(self.bitcoinElectrumUrl, serializer); sse_encode_blockchain_explorer(self.bitcoinExplorer, serializer);
sse_encode_String(self.mempoolspaceUrl, serializer);
sse_encode_String(self.workingDir, serializer); sse_encode_String(self.workingDir, serializer);
sse_encode_opt_String(self.cacheDir, serializer); sse_encode_opt_String(self.cacheDir, serializer);
sse_encode_liquid_network(self.network, serializer); sse_encode_liquid_network(self.network, serializer);

View File

@@ -86,6 +86,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BitcoinAddressData dco_decode_bitcoin_address_data(dynamic raw); BitcoinAddressData dco_decode_bitcoin_address_data(dynamic raw);
@protected
BlockchainExplorer dco_decode_blockchain_explorer(dynamic raw);
@protected @protected
BlockchainInfo dco_decode_blockchain_info(dynamic raw); BlockchainInfo dco_decode_blockchain_info(dynamic raw);
@@ -723,6 +726,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
BitcoinAddressData sse_decode_bitcoin_address_data(SseDeserializer deserializer); BitcoinAddressData sse_decode_bitcoin_address_data(SseDeserializer deserializer);
@protected
BlockchainExplorer sse_decode_blockchain_explorer(SseDeserializer deserializer);
@protected @protected
BlockchainInfo sse_decode_blockchain_info(SseDeserializer deserializer); BlockchainInfo sse_decode_blockchain_info(SseDeserializer deserializer);
@@ -2262,6 +2268,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
wireObj.message = cst_encode_opt_String(apiObj.message); wireObj.message = cst_encode_opt_String(apiObj.message);
} }
@protected
void cst_api_fill_to_wire_blockchain_explorer(
BlockchainExplorer apiObj,
wire_cst_blockchain_explorer wireObj,
) {
if (apiObj is BlockchainExplorer_Electrum) {
var pre_url = cst_encode_String(apiObj.url);
wireObj.tag = 0;
wireObj.kind.Electrum.url = pre_url;
return;
}
if (apiObj is BlockchainExplorer_Esplora) {
var pre_url = cst_encode_String(apiObj.url);
var pre_use_waterfalls = cst_encode_bool(apiObj.useWaterfalls);
wireObj.tag = 1;
wireObj.kind.Esplora.url = pre_url;
wireObj.kind.Esplora.use_waterfalls = pre_use_waterfalls;
return;
}
}
@protected @protected
void cst_api_fill_to_wire_blockchain_info(BlockchainInfo apiObj, wire_cst_blockchain_info wireObj) { void cst_api_fill_to_wire_blockchain_info(BlockchainInfo apiObj, wire_cst_blockchain_info wireObj) {
wireObj.liquid_tip = cst_encode_u_32(apiObj.liquidTip); wireObj.liquid_tip = cst_encode_u_32(apiObj.liquidTip);
@@ -2682,9 +2709,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void cst_api_fill_to_wire_config(Config apiObj, wire_cst_config wireObj) { void cst_api_fill_to_wire_config(Config apiObj, wire_cst_config wireObj) {
wireObj.liquid_electrum_url = cst_encode_String(apiObj.liquidElectrumUrl); cst_api_fill_to_wire_blockchain_explorer(apiObj.liquidExplorer, wireObj.liquid_explorer);
wireObj.bitcoin_electrum_url = cst_encode_String(apiObj.bitcoinElectrumUrl); cst_api_fill_to_wire_blockchain_explorer(apiObj.bitcoinExplorer, wireObj.bitcoin_explorer);
wireObj.mempoolspace_url = cst_encode_String(apiObj.mempoolspaceUrl);
wireObj.working_dir = cst_encode_String(apiObj.workingDir); wireObj.working_dir = cst_encode_String(apiObj.workingDir);
wireObj.cache_dir = cst_encode_opt_String(apiObj.cacheDir); wireObj.cache_dir = cst_encode_opt_String(apiObj.cacheDir);
wireObj.network = cst_encode_liquid_network(apiObj.network); wireObj.network = cst_encode_liquid_network(apiObj.network);
@@ -4077,6 +4103,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected @protected
void sse_encode_bitcoin_address_data(BitcoinAddressData self, SseSerializer serializer); void sse_encode_bitcoin_address_data(BitcoinAddressData self, SseSerializer serializer);
@protected
void sse_encode_blockchain_explorer(BlockchainExplorer self, SseSerializer serializer);
@protected @protected
void sse_encode_blockchain_info(BlockchainInfo self, SseSerializer serializer); void sse_encode_blockchain_info(BlockchainInfo self, SseSerializer serializer);
@@ -7056,6 +7085,30 @@ final class wire_cst_sdk_event extends ffi.Struct {
external SdkEventKind kind; external SdkEventKind kind;
} }
final class wire_cst_BlockchainExplorer_Electrum extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> url;
}
final class wire_cst_BlockchainExplorer_Esplora extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> url;
@ffi.Bool()
external bool use_waterfalls;
}
final class BlockchainExplorerKind extends ffi.Union {
external wire_cst_BlockchainExplorer_Electrum Electrum;
external wire_cst_BlockchainExplorer_Esplora Esplora;
}
final class wire_cst_blockchain_explorer extends ffi.Struct {
@ffi.Int32()
external int tag;
external BlockchainExplorerKind kind;
}
final class wire_cst_external_input_parser extends ffi.Struct { final class wire_cst_external_input_parser extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> provider_id; external ffi.Pointer<wire_cst_list_prim_u_8_strict> provider_id;
@@ -7090,11 +7143,9 @@ final class wire_cst_list_asset_metadata extends ffi.Struct {
} }
final class wire_cst_config extends ffi.Struct { final class wire_cst_config extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> liquid_electrum_url; external wire_cst_blockchain_explorer liquid_explorer;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> bitcoin_electrum_url; external wire_cst_blockchain_explorer bitcoin_explorer;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> mempoolspace_url;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir; external ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir;

View File

@@ -139,6 +139,19 @@ class BackupRequest {
other is BackupRequest && runtimeType == other.runtimeType && backupPath == other.backupPath; other is BackupRequest && runtimeType == other.runtimeType && backupPath == other.backupPath;
} }
@freezed
sealed class BlockchainExplorer with _$BlockchainExplorer {
const BlockchainExplorer._();
const factory BlockchainExplorer.electrum({required String url}) = BlockchainExplorer_Electrum;
const factory BlockchainExplorer.esplora({
required String url,
/// Whether or not to use the "waterfalls" extension
required bool useWaterfalls,
}) = BlockchainExplorer_Esplora;
}
class BlockchainInfo { class BlockchainInfo {
final int liquidTip; final int liquidTip;
final int bitcoinTip; final int bitcoinTip;
@@ -228,11 +241,8 @@ class CheckMessageResponse {
/// Configuration for the Liquid SDK /// Configuration for the Liquid SDK
class Config { class Config {
final String liquidElectrumUrl; final BlockchainExplorer liquidExplorer;
final String bitcoinElectrumUrl; final BlockchainExplorer bitcoinExplorer;
/// The mempool.space API URL, has to be in the format: `https://mempool.space/api`
final String mempoolspaceUrl;
/// Directory in which the DB and log files are stored. /// Directory in which the DB and log files are stored.
/// ///
@@ -282,9 +292,8 @@ class Config {
final List<AssetMetadata>? assetMetadata; final List<AssetMetadata>? assetMetadata;
const Config({ const Config({
required this.liquidElectrumUrl, required this.liquidExplorer,
required this.bitcoinElectrumUrl, required this.bitcoinExplorer,
required this.mempoolspaceUrl,
required this.workingDir, required this.workingDir,
this.cacheDir, this.cacheDir,
required this.network, required this.network,
@@ -300,9 +309,8 @@ class Config {
@override @override
int get hashCode => int get hashCode =>
liquidElectrumUrl.hashCode ^ liquidExplorer.hashCode ^
bitcoinElectrumUrl.hashCode ^ bitcoinExplorer.hashCode ^
mempoolspaceUrl.hashCode ^
workingDir.hashCode ^ workingDir.hashCode ^
cacheDir.hashCode ^ cacheDir.hashCode ^
network.hashCode ^ network.hashCode ^
@@ -320,9 +328,8 @@ class Config {
identical(this, other) || identical(this, other) ||
other is Config && other is Config &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
liquidElectrumUrl == other.liquidElectrumUrl && liquidExplorer == other.liquidExplorer &&
bitcoinElectrumUrl == other.bitcoinElectrumUrl && bitcoinExplorer == other.bitcoinExplorer &&
mempoolspaceUrl == other.mempoolspaceUrl &&
workingDir == other.workingDir && workingDir == other.workingDir &&
cacheDir == other.cacheDir && cacheDir == other.cacheDir &&
network == other.network && network == other.network &&

View File

@@ -12,6 +12,202 @@ part of 'model.dart';
// dart format off // dart format off
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
/// @nodoc
mixin _$BlockchainExplorer {
String get url;
/// Create a copy of BlockchainExplorer
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BlockchainExplorerCopyWith<BlockchainExplorer> get copyWith => _$BlockchainExplorerCopyWithImpl<BlockchainExplorer>(this as BlockchainExplorer, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BlockchainExplorer&&(identical(other.url, url) || other.url == url));
}
@override
int get hashCode => Object.hash(runtimeType,url);
@override
String toString() {
return 'BlockchainExplorer(url: $url)';
}
}
/// @nodoc
abstract mixin class $BlockchainExplorerCopyWith<$Res> {
factory $BlockchainExplorerCopyWith(BlockchainExplorer value, $Res Function(BlockchainExplorer) _then) = _$BlockchainExplorerCopyWithImpl;
@useResult
$Res call({
String url
});
}
/// @nodoc
class _$BlockchainExplorerCopyWithImpl<$Res>
implements $BlockchainExplorerCopyWith<$Res> {
_$BlockchainExplorerCopyWithImpl(this._self, this._then);
final BlockchainExplorer _self;
final $Res Function(BlockchainExplorer) _then;
/// Create a copy of BlockchainExplorer
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? url = null,}) {
return _then(_self.copyWith(
url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class BlockchainExplorer_Electrum extends BlockchainExplorer {
const BlockchainExplorer_Electrum({required this.url}): super._();
@override final String url;
/// Create a copy of BlockchainExplorer
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BlockchainExplorer_ElectrumCopyWith<BlockchainExplorer_Electrum> get copyWith => _$BlockchainExplorer_ElectrumCopyWithImpl<BlockchainExplorer_Electrum>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BlockchainExplorer_Electrum&&(identical(other.url, url) || other.url == url));
}
@override
int get hashCode => Object.hash(runtimeType,url);
@override
String toString() {
return 'BlockchainExplorer.electrum(url: $url)';
}
}
/// @nodoc
abstract mixin class $BlockchainExplorer_ElectrumCopyWith<$Res> implements $BlockchainExplorerCopyWith<$Res> {
factory $BlockchainExplorer_ElectrumCopyWith(BlockchainExplorer_Electrum value, $Res Function(BlockchainExplorer_Electrum) _then) = _$BlockchainExplorer_ElectrumCopyWithImpl;
@override @useResult
$Res call({
String url
});
}
/// @nodoc
class _$BlockchainExplorer_ElectrumCopyWithImpl<$Res>
implements $BlockchainExplorer_ElectrumCopyWith<$Res> {
_$BlockchainExplorer_ElectrumCopyWithImpl(this._self, this._then);
final BlockchainExplorer_Electrum _self;
final $Res Function(BlockchainExplorer_Electrum) _then;
/// Create a copy of BlockchainExplorer
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? url = null,}) {
return _then(BlockchainExplorer_Electrum(
url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class BlockchainExplorer_Esplora extends BlockchainExplorer {
const BlockchainExplorer_Esplora({required this.url, required this.useWaterfalls}): super._();
@override final String url;
/// Whether or not to use the "waterfalls" extension
final bool useWaterfalls;
/// Create a copy of BlockchainExplorer
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$BlockchainExplorer_EsploraCopyWith<BlockchainExplorer_Esplora> get copyWith => _$BlockchainExplorer_EsploraCopyWithImpl<BlockchainExplorer_Esplora>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BlockchainExplorer_Esplora&&(identical(other.url, url) || other.url == url)&&(identical(other.useWaterfalls, useWaterfalls) || other.useWaterfalls == useWaterfalls));
}
@override
int get hashCode => Object.hash(runtimeType,url,useWaterfalls);
@override
String toString() {
return 'BlockchainExplorer.esplora(url: $url, useWaterfalls: $useWaterfalls)';
}
}
/// @nodoc
abstract mixin class $BlockchainExplorer_EsploraCopyWith<$Res> implements $BlockchainExplorerCopyWith<$Res> {
factory $BlockchainExplorer_EsploraCopyWith(BlockchainExplorer_Esplora value, $Res Function(BlockchainExplorer_Esplora) _then) = _$BlockchainExplorer_EsploraCopyWithImpl;
@override @useResult
$Res call({
String url, bool useWaterfalls
});
}
/// @nodoc
class _$BlockchainExplorer_EsploraCopyWithImpl<$Res>
implements $BlockchainExplorer_EsploraCopyWith<$Res> {
_$BlockchainExplorer_EsploraCopyWithImpl(this._self, this._then);
final BlockchainExplorer_Esplora _self;
final $Res Function(BlockchainExplorer_Esplora) _then;
/// Create a copy of BlockchainExplorer
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? url = null,Object? useWaterfalls = null,}) {
return _then(BlockchainExplorer_Esplora(
url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,useWaterfalls: null == useWaterfalls ? _self.useWaterfalls : useWaterfalls // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc /// @nodoc
mixin _$GetPaymentRequest { mixin _$GetPaymentRequest {

View File

@@ -4985,6 +4985,30 @@ final class wire_cst_sdk_event extends ffi.Struct {
external SdkEventKind kind; external SdkEventKind kind;
} }
final class wire_cst_BlockchainExplorer_Electrum extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> url;
}
final class wire_cst_BlockchainExplorer_Esplora extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> url;
@ffi.Bool()
external bool use_waterfalls;
}
final class BlockchainExplorerKind extends ffi.Union {
external wire_cst_BlockchainExplorer_Electrum Electrum;
external wire_cst_BlockchainExplorer_Esplora Esplora;
}
final class wire_cst_blockchain_explorer extends ffi.Struct {
@ffi.Int32()
external int tag;
external BlockchainExplorerKind kind;
}
final class wire_cst_external_input_parser extends ffi.Struct { final class wire_cst_external_input_parser extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> provider_id; external ffi.Pointer<wire_cst_list_prim_u_8_strict> provider_id;
@@ -5019,11 +5043,9 @@ final class wire_cst_list_asset_metadata extends ffi.Struct {
} }
final class wire_cst_config extends ffi.Struct { final class wire_cst_config extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> liquid_electrum_url; external wire_cst_blockchain_explorer liquid_explorer;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> bitcoin_electrum_url; external wire_cst_blockchain_explorer bitcoin_explorer;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> mempoolspace_url;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir; external ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir;

View File

@@ -418,9 +418,8 @@ fun asConfig(config: ReadableMap): Config? {
if (!validateMandatoryFields( if (!validateMandatoryFields(
config, config,
arrayOf( arrayOf(
"liquidElectrumUrl", "liquidExplorer",
"bitcoinElectrumUrl", "bitcoinExplorer",
"mempoolspaceUrl",
"workingDir", "workingDir",
"network", "network",
"paymentTimeoutSec", "paymentTimeoutSec",
@@ -430,9 +429,8 @@ fun asConfig(config: ReadableMap): Config? {
) { ) {
return null return null
} }
val liquidElectrumUrl = config.getString("liquidElectrumUrl")!! val liquidExplorer = config.getMap("liquidExplorer")?.let { asBlockchainExplorer(it) }!!
val bitcoinElectrumUrl = config.getString("bitcoinElectrumUrl")!! val bitcoinExplorer = config.getMap("bitcoinExplorer")?.let { asBlockchainExplorer(it) }!!
val mempoolspaceUrl = config.getString("mempoolspaceUrl")!!
val workingDir = config.getString("workingDir")!! val workingDir = config.getString("workingDir")!!
val network = config.getString("network")?.let { asLiquidNetwork(it) }!! val network = config.getString("network")?.let { asLiquidNetwork(it) }!!
val paymentTimeoutSec = config.getDouble("paymentTimeoutSec").toULong() val paymentTimeoutSec = config.getDouble("paymentTimeoutSec").toULong()
@@ -479,9 +477,8 @@ fun asConfig(config: ReadableMap): Config? {
null null
} }
return Config( return Config(
liquidElectrumUrl, liquidExplorer,
bitcoinElectrumUrl, bitcoinExplorer,
mempoolspaceUrl,
workingDir, workingDir,
network, network,
paymentTimeoutSec, paymentTimeoutSec,
@@ -498,9 +495,8 @@ fun asConfig(config: ReadableMap): Config? {
fun readableMapOf(config: Config): ReadableMap = fun readableMapOf(config: Config): ReadableMap =
readableMapOf( readableMapOf(
"liquidElectrumUrl" to config.liquidElectrumUrl, "liquidExplorer" to readableMapOf(config.liquidExplorer),
"bitcoinElectrumUrl" to config.bitcoinElectrumUrl, "bitcoinExplorer" to readableMapOf(config.bitcoinExplorer),
"mempoolspaceUrl" to config.mempoolspaceUrl,
"workingDir" to config.workingDir, "workingDir" to config.workingDir,
"network" to config.network.name.lowercase(), "network" to config.network.name.lowercase(),
"paymentTimeoutSec" to config.paymentTimeoutSec, "paymentTimeoutSec" to config.paymentTimeoutSec,
@@ -2994,6 +2990,48 @@ fun asAmountList(arr: ReadableArray): List<Amount> {
return list return list
} }
fun asBlockchainExplorer(blockchainExplorer: ReadableMap): BlockchainExplorer? {
val type = blockchainExplorer.getString("type")
if (type == "electrum") {
val url = blockchainExplorer.getString("url")!!
return BlockchainExplorer.Electrum(url)
}
if (type == "esplora") {
val url = blockchainExplorer.getString("url")!!
val useWaterfalls = blockchainExplorer.getBoolean("useWaterfalls")
return BlockchainExplorer.Esplora(url, useWaterfalls)
}
return null
}
fun readableMapOf(blockchainExplorer: BlockchainExplorer): ReadableMap? {
val map = Arguments.createMap()
when (blockchainExplorer) {
is BlockchainExplorer.Electrum -> {
pushToMap(map, "type", "electrum")
pushToMap(map, "url", blockchainExplorer.url)
}
is BlockchainExplorer.Esplora -> {
pushToMap(map, "type", "esplora")
pushToMap(map, "url", blockchainExplorer.url)
pushToMap(map, "useWaterfalls", blockchainExplorer.useWaterfalls)
}
}
return map
}
fun asBlockchainExplorerList(arr: ReadableArray): List<BlockchainExplorer> {
val list = ArrayList<BlockchainExplorer>()
for (value in arr.toList()) {
when (value) {
is ReadableMap -> list.add(asBlockchainExplorer(value)!!)
else -> throw SdkException.Generic(errUnexpectedType(value))
}
}
return list
}
fun asBuyBitcoinProvider(type: String): BuyBitcoinProvider = BuyBitcoinProvider.valueOf(camelToUpperSnakeCase(type)) fun asBuyBitcoinProvider(type: String): BuyBitcoinProvider = BuyBitcoinProvider.valueOf(camelToUpperSnakeCase(type))
fun asBuyBitcoinProviderList(arr: ReadableArray): List<BuyBitcoinProvider> { fun asBuyBitcoinProviderList(arr: ReadableArray): List<BuyBitcoinProvider> {

View File

@@ -492,15 +492,16 @@ enum BreezSDKLiquidMapper {
} }
static func asConfig(config: [String: Any?]) throws -> Config { static func asConfig(config: [String: Any?]) throws -> Config {
guard let liquidElectrumUrl = config["liquidElectrumUrl"] as? String else { guard let liquidExplorerTmp = config["liquidExplorer"] as? [String: Any?] else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "liquidElectrumUrl", typeName: "Config")) throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "liquidExplorer", typeName: "Config"))
} }
guard let bitcoinElectrumUrl = config["bitcoinElectrumUrl"] as? String else { let liquidExplorer = try asBlockchainExplorer(blockchainExplorer: liquidExplorerTmp)
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "bitcoinElectrumUrl", typeName: "Config"))
} guard let bitcoinExplorerTmp = config["bitcoinExplorer"] as? [String: Any?] else {
guard let mempoolspaceUrl = config["mempoolspaceUrl"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "bitcoinExplorer", typeName: "Config"))
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "mempoolspaceUrl", typeName: "Config"))
} }
let bitcoinExplorer = try asBlockchainExplorer(blockchainExplorer: bitcoinExplorerTmp)
guard let workingDir = config["workingDir"] as? String else { guard let workingDir = config["workingDir"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "workingDir", typeName: "Config")) throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "workingDir", typeName: "Config"))
} }
@@ -560,14 +561,13 @@ enum BreezSDKLiquidMapper {
assetMetadata = try asAssetMetadataList(arr: assetMetadataTmp) assetMetadata = try asAssetMetadataList(arr: assetMetadataTmp)
} }
return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte, assetMetadata: assetMetadata) return Config(liquidExplorer: liquidExplorer, bitcoinExplorer: bitcoinExplorer, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte, assetMetadata: assetMetadata)
} }
static func dictionaryOf(config: Config) -> [String: Any?] { static func dictionaryOf(config: Config) -> [String: Any?] {
return [ return [
"liquidElectrumUrl": config.liquidElectrumUrl, "liquidExplorer": dictionaryOf(blockchainExplorer: config.liquidExplorer),
"bitcoinElectrumUrl": config.bitcoinElectrumUrl, "bitcoinExplorer": dictionaryOf(blockchainExplorer: config.bitcoinExplorer),
"mempoolspaceUrl": config.mempoolspaceUrl,
"workingDir": config.workingDir, "workingDir": config.workingDir,
"network": valueOf(liquidNetwork: config.network), "network": valueOf(liquidNetwork: config.network),
"paymentTimeoutSec": config.paymentTimeoutSec, "paymentTimeoutSec": config.paymentTimeoutSec,
@@ -3484,6 +3484,65 @@ enum BreezSDKLiquidMapper {
return list return list
} }
static func asBlockchainExplorer(blockchainExplorer: [String: Any?]) throws -> BlockchainExplorer {
let type = blockchainExplorer["type"] as! String
if type == "electrum" {
guard let _url = blockchainExplorer["url"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "url", typeName: "BlockchainExplorer"))
}
return BlockchainExplorer.electrum(url: _url)
}
if type == "esplora" {
guard let _url = blockchainExplorer["url"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "url", typeName: "BlockchainExplorer"))
}
guard let _useWaterfalls = blockchainExplorer["useWaterfalls"] as? Bool else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "useWaterfalls", typeName: "BlockchainExplorer"))
}
return BlockchainExplorer.esplora(url: _url, useWaterfalls: _useWaterfalls)
}
throw SdkError.Generic(message: "Unexpected type \(type) for enum BlockchainExplorer")
}
static func dictionaryOf(blockchainExplorer: BlockchainExplorer) -> [String: Any?] {
switch blockchainExplorer {
case let .electrum(
url
):
return [
"type": "electrum",
"url": url,
]
case let .esplora(
url, useWaterfalls
):
return [
"type": "esplora",
"url": url,
"useWaterfalls": useWaterfalls,
]
}
}
static func arrayOf(blockchainExplorerList: [BlockchainExplorer]) -> [Any] {
return blockchainExplorerList.map { v -> [String: Any?] in return dictionaryOf(blockchainExplorer: v) }
}
static func asBlockchainExplorerList(arr: [Any]) throws -> [BlockchainExplorer] {
var list = [BlockchainExplorer]()
for value in arr {
if let val = value as? [String: Any?] {
var blockchainExplorer = try asBlockchainExplorer(blockchainExplorer: val)
list.append(blockchainExplorer)
} else {
throw SdkError.Generic(message: errUnexpectedType(typeName: "BlockchainExplorer"))
}
}
return list
}
static func asBuyBitcoinProvider(buyBitcoinProvider: String) throws -> BuyBitcoinProvider { static func asBuyBitcoinProvider(buyBitcoinProvider: String) throws -> BuyBitcoinProvider {
switch buyBitcoinProvider { switch buyBitcoinProvider {
case "moonpay": case "moonpay":

View File

@@ -88,9 +88,8 @@ export interface CheckMessageResponse {
} }
export interface Config { export interface Config {
liquidElectrumUrl: string liquidExplorer: BlockchainExplorer
bitcoinElectrumUrl: string bitcoinExplorer: BlockchainExplorer
mempoolspaceUrl: string
workingDir: string workingDir: string
network: LiquidNetwork network: LiquidNetwork
paymentTimeoutSec: number paymentTimeoutSec: number
@@ -519,6 +518,20 @@ export type Amount = {
fractionalAmount: number fractionalAmount: number
} }
export enum BlockchainExplorerVariant {
ELECTRUM = "electrum",
ESPLORA = "esplora"
}
export type BlockchainExplorer = {
type: BlockchainExplorerVariant.ELECTRUM,
url: string
} | {
type: BlockchainExplorerVariant.ESPLORA,
url: string
useWaterfalls: boolean
}
export enum BuyBitcoinProvider { export enum BuyBitcoinProvider {
MOONPAY = "moonpay" MOONPAY = "moonpay"
} }

View File

@@ -1,4 +1 @@
MEMPOOL_WEB_IMAGE=mempool/frontend:latest
MEMPOOL_API_IMAGE=mempool/backend:latest
MEMPOOL_DB_IMAGE=mariadb:10.5.21
RT_SYNC_IMAGE=danielgranhao/data-sync:latest RT_SYNC_IMAGE=danielgranhao/data-sync:latest

4
regtest/.gitignore vendored
View File

@@ -1,8 +1,4 @@
.DS_Store .DS_Store
.idea/ .idea/
data/mempool/*
!data/mempool/.gitkeep
data/mempool-db/*
!data/mempool-db/.gitkeep
data/rt-sync/* data/rt-sync/*
!data/rt-sync/.gitkeep !data/rt-sync/.gitkeep

View File

@@ -1,51 +1,4 @@
services: services:
mempool-web:
environment:
FRONTEND_HTTP_PORT: "8080"
BACKEND_MAINNET_HTTP_HOST: "mempool-api"
image: ${MEMPOOL_WEB_IMAGE}
user: "0:0"
restart: on-failure
command: "./wait-for mempool-db:3306 --timeout=720 -- nginx -g 'daemon off;'"
ports:
- 80:8080
mempool-api:
environment:
MEMPOOL_NETWORK: "regtest"
MEMPOOL_BACKEND: "electrum"
CORE_RPC_HOST: "bitcoind"
CORE_RPC_PORT: "18443"
CORE_RPC_COOKIE: "true"
CORE_RPC_COOKIE_PATH: "/root/.bitcoin/regtest/.cookie"
CORE_RPC_TIMEOUT: "60000"
ELECTRUM_HOST: "electrs"
ELECTRUM_PORT: "19001"
ELECTRUM_TLS_ENABLED: "false"
DATABASE_ENABLED: "true"
DATABASE_HOST: "mempool-db"
DATABASE_DATABASE: "mempool"
DATABASE_USERNAME: "mempool"
DATABASE_PASSWORD: "mempool"
STATISTICS_ENABLED: "true"
image: ${MEMPOOL_API_IMAGE}
user: "0:0"
restart: on-failure
command: "./wait-for-it.sh mempool-db:3306 --timeout=720 --strict -- ./start.sh"
volumes:
- mempool-data:/backend/cache
- bitcoin-data:/root/.bitcoin
mempool-db:
environment:
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "admin"
image: ${MEMPOOL_DB_IMAGE}
user: "0:0"
restart: on-failure
volumes:
- mempool-db-data:/var/lib/mysql
rt-sync: rt-sync:
environment: environment:
SQLITE_DIR_PATH: /app/db SQLITE_DIR_PATH: /app/db