From 0017f7d3f76a1f0094ad9ff25422b72c31acc60e Mon Sep 17 00:00:00 2001 From: Ross Savage <551697+dangeross@users.noreply.github.com> Date: Wed, 5 Mar 2025 12:41:47 +0100 Subject: [PATCH] Introduce a replaceable RestClient (#1175) * Integrate RestClient trait * Cleanup docs and redundant response status * Fix C# CI test * Cargo fmt * Fix withdraw timeout handling * Pass RestClient impl as reference * Remove domain validation * Simplify RestClient interface naming * Move success check to another fn * Move check outside of trait * Remove superfluous test dependencies * Improve tests --- .github/workflows/main.yml | 4 +- libs/Cargo.lock | 57 -- libs/Cargo.toml | 1 - libs/sdk-common/Cargo.toml | 12 +- libs/sdk-common/mock_service_worker.js | 97 --- .../src/dns_resolver/resolver_wasm.rs | 5 +- libs/sdk-common/src/input_parser.rs | 589 ++++++++++-------- libs/sdk-common/src/lib.rs | 2 + libs/sdk-common/src/liquid/mod.rs | 7 +- libs/sdk-common/src/lnurl/error.rs | 8 +- libs/sdk-common/src/lnurl/mod.rs | 32 - libs/sdk-common/src/lnurl/specs/auth.rs | 9 +- libs/sdk-common/src/lnurl/specs/pay.rs | 12 +- libs/sdk-common/src/lnurl/specs/withdraw.rs | 74 ++- .../src/test_utils/mock_rest_client.rs | 64 ++ libs/sdk-common/src/test_utils/mock_server.rs | 47 -- .../src/test_utils/mock_server_wasm.rs | 101 --- libs/sdk-common/src/test_utils/mod.rs | 9 +- libs/sdk-common/src/utils/rest_client.rs | 135 ++-- libs/sdk-core/Cargo.toml | 2 +- libs/sdk-core/src/breez_services.rs | 91 ++- libs/sdk-core/src/chain.rs | 216 +++++-- libs/sdk-core/src/error.rs | 6 + libs/sdk-core/src/lnurl/mod.rs | 18 - libs/sdk-core/src/lnurl/pay.rs | 495 +++++++-------- libs/sdk-core/src/swap_out/boltzswap.rs | 142 ++--- libs/sdk-core/src/swap_out/error.rs | 8 + 27 files changed, 1046 insertions(+), 1197 deletions(-) delete mode 100644 libs/sdk-common/mock_service_worker.js create mode 100644 libs/sdk-common/src/test_utils/mock_rest_client.rs delete mode 100644 libs/sdk-common/src/test_utils/mock_server.rs delete mode 100644 libs/sdk-common/src/test_utils/mock_server_wasm.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de3a44fa7..aa3080597 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,6 +71,8 @@ jobs: build-bindings: name: Test sdk-bindings runs-on: macOS-latest + env: + DOTNET_INSTALL_DIR: "${{ github.workspace }}/dotnet" steps: - name: Checkout repo uses: actions/checkout@v4 @@ -112,7 +114,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '7.0.x' - name: Setup go uses: actions/setup-go@v5 diff --git a/libs/Cargo.lock b/libs/Cargo.lock index ea3d33966..3e1b77e1b 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -264,16 +264,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -683,7 +673,6 @@ dependencies = [ "lazy_static", "log", "miniz_oxide 0.7.4", - "mockito", "once_cell", "openssl", "prost 0.11.9", @@ -938,16 +927,6 @@ dependencies = [ "tonic-build 0.8.4", ] -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1935,7 +1914,6 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -2588,30 +2566,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "mockito" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" -dependencies = [ - "assert-json-diff", - "bytes", - "colored", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.5.2", - "hyper-util", - "log", - "rand", - "regex", - "serde_json", - "serde_urlencoded", - "similar", - "tokio", -] - [[package]] name = "multimap" version = "0.8.3" @@ -3807,14 +3761,11 @@ dependencies = [ "getrandom", "hex", "hickory-resolver", - "js-sys", "lazy_static", "lightning 0.0.118", "lightning 0.0.125", "lightning-invoice 0.26.0", "log", - "mockito", - "once_cell", "percent-encoding", "prost 0.11.9", "prost 0.13.5", @@ -3835,9 +3786,7 @@ dependencies = [ "url", "urlencoding", "wasm-bindgen", - "wasm-bindgen-futures", "wasm-bindgen-test", - "web-sys", ] [[package]] @@ -4154,12 +4103,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "similar" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" - [[package]] name = "simple_asn1" version = "0.5.4" diff --git a/libs/Cargo.toml b/libs/Cargo.toml index 01dc55e4d..bd58b44a4 100644 --- a/libs/Cargo.toml +++ b/libs/Cargo.toml @@ -27,7 +27,6 @@ hex = "0.4" lightning = "=0.0.118" # Same version as used in gl-client lightning-invoice = "=0.26.0" # Same version as used in gl-client log = "0.4" -mockito = "1" once_cell = "1" prost = "^0.11" regex = "1.8.1" diff --git a/libs/sdk-common/Cargo.toml b/libs/sdk-common/Cargo.toml index 3c92490c7..ea1bfb328 100644 --- a/libs/sdk-common/Cargo.toml +++ b/libs/sdk-common/Cargo.toml @@ -51,23 +51,12 @@ wasm-bindgen = "0.2.100" [dev-dependencies] bitcoin = { workspace = true, features = ["rand"] } -once_cell = { workspace = true } [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies] -mockito = { workspace = true } tokio = { workspace = true, features = ["sync", "rt"] } [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dev-dependencies] -js-sys = "0.3" wasm-bindgen-test = "0.3.33" -wasm-bindgen-futures = "0.4" -web-sys = { version = "0.3", features = [ - "Navigator", - "ServiceWorkerContainer", - "ServiceWorker", - "ServiceWorkerRegistration", - "ServiceWorkerState", -] } [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.build-dependencies] tonic-build = { workspace = true } @@ -77,3 +66,4 @@ tonic-build = "0.12" [features] liquid = ["dep:elements", "dep:lightning-125"] +test-utils = [] diff --git a/libs/sdk-common/mock_service_worker.js b/libs/sdk-common/mock_service_worker.js deleted file mode 100644 index 289a21d08..000000000 --- a/libs/sdk-common/mock_service_worker.js +++ /dev/null @@ -1,97 +0,0 @@ -let db = null; - -async function getDb() { - if (db) { - return db; - } else { - return await new Promise((resolve, reject) => { - const req = indexedDB.open("http_mock", 1); - req.onsuccess = (event) => resolve(event.target.result); - req.onerror = reject; - req.onupgradeneeded = (event) => { - db = event.target.result; - if (!db.objectStoreNames.contains("mocks")) { - db.createObjectStore("mocks", { keyPath: "nonce" }); - } - }; - }) - } -} - -async function setMock(mock) { - const db = await getDb(); - await new Promise((resolve, reject) => { - const transaction = db.transaction("mocks", "readwrite"); - transaction.oncomplete = resolve; - transaction.onerror = reject; - const store = transaction.objectStore("mocks"); - store.put(mock); - }) -} - -async function getMock(nonce) { - const db = await getDb(); - return await new Promise((resolve, reject) => { - const req = db.transaction("mocks") - .objectStore("mocks") - .get(nonce); - req.onsuccess = (event) => resolve(event.target.result); - req.onerror = reject; - }); -} - -// Status codes are chosen to avoid being picked up as successes by tests expecting a 404 or 500. - -self.addEventListener("fetch", (event) => { - event.respondWith((async () => { - try { - const request = event.request; - const url = new URL(request.url); - if (url.host === "mock_configure") { - const nonce = url.pathname.substring(1); - const { method, path, status_code, body, content_type } = await request.json(); - const mock = await getMock(nonce) ?? { nonce, routes: [] }; - mock.routes.push({ method, path, status_code, body, content_type, hits: 0 }); - await setMock(mock); - return new Response(null, { status: 204 }); - } else if (url.host === "mock_assert") { - const nonce = url.pathname.substring(1); - const mock = await getMock(nonce); - if (mock === undefined) { - return new Response(`no such mock id ${nonce}`, { status: 421 }); - } - const hitsMap = Object.fromEntries(mock.routes.map(route => [`${route.method} ${route.path}`, route.hits])); - return new Response(JSON.stringify(hitsMap), { status: 200, headers: { 'Content-Type': 'application/json' } }); - } else { - const nonce = url.host.split('_')[1]; - const mock = await getMock(nonce); - if (mock === undefined) { - return new Response(`no such mock id ${nonce}`, { status: 421 }); - } - for (const route of mock.routes) { - const partialPath = route.path.split('?')[0]; - // Replace only -.!~*'() - const decodedPartialPath = route.path.replaceAll('%21', '!').replaceAll('%27', '\'').replaceAll('%28', '(').replaceAll('%29', ')').replaceAll('%2A', '*').replaceAll('%2D', '-').replaceAll('%2E', '.'); - if (request.method === route.method && (url.pathname === route.path || url.pathname === partialPath || url.pathname === decodedPartialPath)) { - route.hits += 1; - await setMock(mock); - return new Response(Uint8Array.from(route.body), { status: route.status_code, headers: { 'Content-Type': route.content_type || 'application/json' }}); - } - } - const possiblyMeant = mock.routes.find(route => route.path === url.pathname); - if (possiblyMeant !== undefined) { - return new Response(`expected ${possiblyMeant.method}, got ${request.method}`, { status: 405 }) - } else { - return new Response(`expected ${mock.routes.map(route => route.path).join(' | ')}, got ${url.pathname}`, { status: 410 }); - } - } - } catch (e) { - return new Response(e.toString(), { status: 503 }); - } - })()) -}); - -self.addEventListener("activate", (event) => { - skipWaiting(); - event.waitUntil(clients.claim()); -}); \ No newline at end of file diff --git a/libs/sdk-common/src/dns_resolver/resolver_wasm.rs b/libs/sdk-common/src/dns_resolver/resolver_wasm.rs index e63a11043..81c9429e0 100644 --- a/libs/sdk-common/src/dns_resolver/resolver_wasm.rs +++ b/libs/sdk-common/src/dns_resolver/resolver_wasm.rs @@ -1,8 +1,7 @@ use anyhow::{anyhow, Result}; use dns_parser::{Builder, Packet, RData, ResponseCode}; use dns_parser::{QueryClass, QueryType}; - -use crate::utils::rest_client; +use reqwest::Client; pub(crate) async fn txt_lookup(dns_name: String) -> Result> { let mut builder = Builder::new_query(1, true); @@ -10,7 +9,7 @@ pub(crate) async fn txt_lookup(dns_name: String) -> Result> { let req_bytes = builder .build() .map_err(|_| anyhow!("Error building DNS query"))?; - let client = rest_client::get_reqwest_client()?; + let client = Client::builder().build()?; let res_bytes = client .post("https://cloudflare-dns.com/dns-query") .body(req_bytes) diff --git a/libs/sdk-common/src/input_parser.rs b/libs/sdk-common/src/input_parser.rs index 3b07ab891..56065c137 100644 --- a/libs/sdk-common/src/input_parser.rs +++ b/libs/sdk-common/src/input_parser.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; use std::ops::Not; use std::str::FromStr; +use std::{collections::HashMap, sync::Arc}; use ::bip21::Uri; use anyhow::{anyhow, bail, Context, Result}; @@ -104,10 +104,10 @@ const BIP353_PREFIX: &str = "bitcoin:"; /// let lnurl_pay_url = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf"; /// /// assert!(matches!( parse(lnurl_pay_url, None).await, Ok(LnUrlPay{data: _, bip353_address: _}) )); -/// // assert!(matches!( parse("lnurlp://domain.com/lnurl-pay?key=val").await, Ok(LnUrlPay{data: _}) )); -/// // assert!(matches!( parse("lightning@address.com").await, Ok(LnUrlPay{data: _}) )); +/// // assert!(matches!( parse("lnurlp://domain.com/lnurl-pay?key=val", None).await, Ok(LnUrlPay{data: _}) )); +/// // assert!(matches!( parse("lightning@address.com", None).await, Ok(LnUrlPay{data: _}) )); /// -/// if let Ok(LnUrlPay{data: pd, bip353_address}) = parse(lnurl_pay_url,None).await { +/// if let Ok(LnUrlPay{data: pd, bip353_address}) = parse(lnurl_pay_url, None).await { /// assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); /// assert_eq!(pd.max_sendable, 16000); // Max sendable amount, in msats /// assert_eq!(pd.max_sendable_sats(), 16); // Max sendable amount, in sats @@ -131,9 +131,8 @@ const BIP353_PREFIX: &str = "bitcoin:"; /// let lnurl_withdraw_url = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; /// /// assert!(matches!( parse(lnurl_withdraw_url, None).await, Ok(LnUrlWithdraw{data: _}) )); -/// // assert!(matches!( parse("lnurlw://domain.com/lnurl-withdraw?key=val").await, Ok(LnUrlWithdraw{data: _} )); /// -/// if let Ok(LnUrlWithdraw{data: wd}) = parse(lnurl_withdraw_url,None).await { +/// if let Ok(LnUrlWithdraw{data: wd}) = parse(lnurl_withdraw_url, None).await { /// assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); /// assert_eq!(wd.k1, "37b4c919f871c090830cc47b92a544a30097f03430bc39670b8ec0da89f01a81"); /// assert_eq!(wd.min_withdrawable, 3000); // Min withdrawable amount, in msats @@ -155,9 +154,8 @@ const BIP353_PREFIX: &str = "bitcoin:"; /// let lnurl_auth_url = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgeqgntfgu"; /// /// assert!(matches!( parse(lnurl_auth_url, None).await, Ok(LnUrlAuth{data: _}) )); -/// // assert!(matches!( parse("keyauth://domain.com/auth?key=val").await, Ok(LnUrlAuth{data: _}) )); /// -/// if let Ok(LnUrlAuth{data: ad}) = parse(lnurl_auth_url,None).await { +/// if let Ok(LnUrlAuth{data: ad}) = parse(lnurl_auth_url, None).await { /// assert_eq!(ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822"); /// } /// } @@ -166,7 +164,7 @@ const BIP353_PREFIX: &str = "bitcoin:"; /// ## External input parsing /// /// ```no_run -/// use sdk_common::prelude::{InputType::*, parse, ExternalInputParser}; +/// use sdk_common::prelude::{ExternalInputParser, InputType::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -188,6 +186,15 @@ const BIP353_PREFIX: &str = "bitcoin:"; pub async fn parse( input: &str, external_input_parsers: Option<&[ExternalInputParser]>, +) -> Result { + let rest_client: Arc = Arc::new(ReqwestRestClient::new()?); + parse_with_rest_client(rest_client.as_ref(), input, external_input_parsers).await +} + +pub async fn parse_with_rest_client( + rest_client: &C, + input: &str, + external_input_parsers: Option<&[ExternalInputParser]>, ) -> Result { let input = input.trim(); @@ -197,7 +204,7 @@ pub async fn parse( None => (input.to_string(), false), }; - if let Ok(input_type) = parse_core(&bip353_parsed_input).await { + if let Ok(input_type) = parse_core(rest_client, &bip353_parsed_input).await { let input_type = if is_bip353 { match input_type { #[cfg(feature = "liquid")] @@ -219,7 +226,7 @@ pub async fn parse( } if let Some(external_input_parsers) = external_input_parsers { - return parse_external(input, external_input_parsers).await; + return parse_external(rest_client, input, external_input_parsers).await; } Err(anyhow!("Unrecognized input type")) @@ -293,7 +300,7 @@ async fn bip353_parse(input: &str) -> Option { } /// Core parse implementation -async fn parse_core(input: &str) -> Result { +async fn parse_core(rest_client: &C, input: &str) -> Result { // Covers BIP 21 URIs and simple onchain BTC addresses (which are valid BIP 21 with the 'bitcoin:' prefix) if let Ok(bip21_uri) = prepend_if_missing("bitcoin:", input).parse::>() { let bitcoin_addr_data = bip21_uri.into(); @@ -357,7 +364,7 @@ async fn parse_core(input: &str) -> Result { .find(|p| p.0 == "lightning" || p.0 == "LIGHTNING") { if let Ok((domain, lnurl_endpoint, ln_address)) = lnurl_decode(&value) { - return resolve_lnurl(domain, lnurl_endpoint, ln_address).await; + return resolve_lnurl(rest_client, domain, lnurl_endpoint, ln_address).await; } } return Ok(InputType::Url { url: input.into() }); @@ -370,14 +377,15 @@ async fn parse_core(input: &str) -> Result { .or(input.strip_prefix("LIGHTNING:")) .unwrap_or(input); if let Ok((domain, lnurl_endpoint, ln_address)) = lnurl_decode(input) { - return resolve_lnurl(domain, lnurl_endpoint, ln_address).await; + return resolve_lnurl(rest_client, domain, lnurl_endpoint, ln_address).await; } Err(anyhow!("Unrecognized input type")) } /// Parse input using provided external parsers. -async fn parse_external( +async fn parse_external( + rest_client: &C, input: &str, external_input_parsers: &[ExternalInputParser], ) -> Result { @@ -395,11 +403,10 @@ async fn parse_external( let urlsafe_input = percent_encoding::utf8_percent_encode(input, NON_ALPHANUMERIC).to_string(); let parser_url = parser.parser_url.replacen("", &urlsafe_input, 1); - let parser_url = maybe_replace_host_with_mock_test_host(parser_url)?; // Make request - let parsed_value = match request_external_parsing(&parser_url).await { - Ok(t) => t, + let (response, _) = match get_and_check_success(rest_client, &parser_url).await { + Ok(response) => response, Err(e) => { error!("Request to external input parser {parser:?} failed: {e}"); continue; @@ -407,7 +414,7 @@ async fn parse_external( }; // Try to parse as LnUrlRequestData - if let Ok(lnurl_data) = serde_json::from_str::(&parsed_value) { + if let Ok(lnurl_data) = serde_json::from_str::(&response) { let domain = url::Url::parse(&parser_url) .ok() .and_then(|url| url.host_str().map(|s| s.to_string())) @@ -425,7 +432,7 @@ async fn parse_external( } // Check other input types - if let Ok(input_type) = parse_core(&parsed_value).await { + if let Ok(input_type) = parse_core(rest_client, &response).await { return Ok(input_type); } } @@ -433,11 +440,6 @@ async fn parse_external( Err(anyhow!("Unrecognized input type")) } -async fn request_external_parsing(url: &str) -> reqwest::Result { - let response = reqwest::get(url).await?.error_for_status()?; - response.text().await -} - /// Prepends the given prefix to the input, if the input doesn't already start with it fn prepend_if_missing(prefix: &str, input: &str) -> String { match input.to_lowercase().starts_with(prefix) { @@ -572,9 +574,10 @@ fn lnurl_decode(encoded: &str) -> LnUrlResult<(String, String, Option)> } } -async fn resolve_lnurl( +async fn resolve_lnurl( + rest_client: &C, domain: String, - mut lnurl_endpoint: String, + lnurl_endpoint: String, ln_address: Option, ) -> Result { // For LNURL-auth links, their type is already known if the link contains the login tag @@ -585,10 +588,9 @@ async fn resolve_lnurl( }); } - lnurl_endpoint = maybe_replace_host_with_mock_test_host(lnurl_endpoint)?; - let lnurl_data: LnUrlRequestData = get_parse_and_log_response(&lnurl_endpoint, false) - .await - .map_err(|_| anyhow!("Failed to parse response"))?; + let (response, _) = rest_client.get(&lnurl_endpoint).await?; + let lnurl_data: LnUrlRequestData = + parse_json(&response).map_err(|_| anyhow!("Failed to parse response"))?; let temp = lnurl_data.into(); let temp = match temp { // Modify the LnUrlPay payload by adding the domain of the LNURL endpoint @@ -897,35 +899,32 @@ pub(crate) mod tests { use bitcoin::bech32; use bitcoin::bech32::{ToBase32, Variant}; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; - use once_cell::sync::Lazy; + use serde_json::json; use crate::input_parser::*; - use crate::test_utils::mock_server::*; + use crate::test_utils::mock_rest_client::{MockResponse, MockRestClient}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - /// Mock server used in tests. As the server is shared between tests, - /// we should not mock the same url twice with two different outputs, - /// one way to do so is to add a random string that will be a differentiator - /// in the URL. - pub(crate) static MOCK_HTTP_SERVER: Lazy = Lazy::new(|| { - let opts = ServerOpts { - host: "127.0.0.1", - port: 8080, - }; - MockServer::new_with_opts(opts) - }); - #[sdk_macros::async_test_all] async fn test_generic_invalid_input() -> Result<(), Box> { - assert!(parse("invalid_input", None).await.is_err()); + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); + + assert!( + parse_with_rest_client(rest_client.as_ref(), "invalid_input", None) + .await + .is_err() + ); Ok(()) } #[sdk_macros::async_test_all] async fn test_trim_input() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); for address in [ r#"1andreas3batLhQa2FawWjeyjCqyBzypd"#, r#"1andreas3batLhQa2FawWjeyjCqyBzypd "#, @@ -938,7 +937,7 @@ pub(crate) mod tests { "#, ] { assert!(matches!( - parse(address, None).await?, + parse_with_rest_client(rest_client.as_ref(), address, None).await?, InputType::BitcoinAddress { address: _ } )); } @@ -947,6 +946,8 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_bitcoin_address() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); for address in [ "1andreas3batLhQa2FawWjeyjCqyBzypd", "12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX", @@ -954,7 +955,7 @@ pub(crate) mod tests { "3CJ7cNxChpcUykQztFSqKFrMVQDN4zTTsp", ] { assert!(matches!( - parse(address, None).await?, + parse_with_rest_client(rest_client.as_ref(), address, None).await?, InputType::BitcoinAddress { address: _ } )); } @@ -963,19 +964,29 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_bitcoin_address_bip21() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); // Addresses from https://github.com/Kixunil/bip21/blob/master/src/lib.rs // Valid address with the `bitcoin:` prefix - assert!(parse("bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd", None) - .await - .is_ok()); - assert!(parse("bitcoin:testinvalidaddress", None).await.is_err()); + assert!(parse_with_rest_client( + rest_client.as_ref(), + "bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd", + None + ) + .await + .is_ok()); + assert!( + parse_with_rest_client(rest_client.as_ref(), "bitcoin:testinvalidaddress", None) + .await + .is_err() + ); let addr = "1andreas3batLhQa2FawWjeyjCqyBzypd"; // Address with amount let addr_1 = format!("bitcoin:{addr}?amount=0.00002000"); - match parse(&addr_1, None).await? { + match parse_with_rest_client(rest_client.as_ref(), &addr_1, None).await? { InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { @@ -991,7 +1002,7 @@ pub(crate) mod tests { // Address with amount and label let label = "test-label"; let addr_2 = format!("bitcoin:{addr}?amount=0.00002000&label={label}"); - match parse(&addr_2, None).await? { + match parse_with_rest_client(rest_client.as_ref(), &addr_2, None).await? { InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { @@ -1007,7 +1018,7 @@ pub(crate) mod tests { // Address with amount, label and message let message = "test-message"; let addr_3 = format!("bitcoin:{addr}?amount=0.00002000&label={label}&message={message}"); - match parse(&addr_3, None).await? { + match parse_with_rest_client(rest_client.as_ref(), &addr_3, None).await? { InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { @@ -1035,10 +1046,12 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_bitcoin_address_bip21_rounding() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); for (amount_sat, amount_btc) in get_bip21_rounding_test_vectors() { let addr = format!("bitcoin:1andreas3batLhQa2FawWjeyjCqyBzypd?amount={amount_btc}"); - match parse(&addr, None).await? { + match parse_with_rest_client(rest_client.as_ref(), &addr, None).await? { InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { @@ -1054,23 +1067,30 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] #[cfg(feature = "liquid")] async fn test_liquid_address() -> Result<()> { - assert!(parse("tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk", None) + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); + + assert!(parse_with_rest_client(rest_client.as_ref(), "tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk", None) .await .is_ok()); - assert!(parse("liquidnetwork:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk", None) + assert!(parse_with_rest_client(rest_client.as_ref(), "liquidnetwork:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk", None) .await .is_ok()); - assert!(parse("wrong-net:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk", None).await.is_err()); - assert!(parse("liquidnetwork:testinvalidaddress", None) - .await - .is_err()); + assert!(parse_with_rest_client(rest_client.as_ref(), "wrong-net:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk", None).await.is_err()); + assert!(parse_with_rest_client( + rest_client.as_ref(), + "liquidnetwork:testinvalidaddress", + None + ) + .await + .is_err()); let address: elements::Address = "tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk".parse()?; let amount_btc = 0.00001; // 1000 sats let label = "label"; let message = "this%20is%20a%20message"; let asset_id = elements::issuance::AssetId::LIQUID_BTC.to_string(); - let output = parse(&format!( + let output = parse_with_rest_client(rest_client.as_ref(), &format!( "liquidnetwork:{}?amount={amount_btc}&assetid={asset_id}&label={label}&message={message}", address ), @@ -1101,18 +1121,20 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_bolt11() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); let bolt11 = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; // Invoice without prefix assert!(matches!( - parse(bolt11, None).await?, + parse_with_rest_client(rest_client.as_ref(), bolt11, None).await?, InputType::Bolt11 { invoice: _invoice } )); // Invoice with prefix let invoice_with_prefix = format!("lightning:{bolt11}"); assert!(matches!( - parse(&invoice_with_prefix, None).await?, + parse_with_rest_client(rest_client.as_ref(), &invoice_with_prefix, None).await?, InputType::Bolt11 { invoice: _invoice } )); @@ -1121,18 +1143,20 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_capitalized_bolt11() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); let bolt11 = "LNBC110N1P38Q3GTPP5YPZ09JRD8P993SNJWNM68CPH4FTWP22LE34XD4R8FTSPWSHXHMNSDQQXQYJW5QCQPXSP5HTLG8YDPYWVSA7H3U4HDN77EHS4Z4E844EM0APJYVMQFKZQHHD2Q9QGSQQQYSSQSZPXZXT9UUQZYMR7ZXCDCCJ5G69S8Q7ZZJS7SGXN9EJHNVDH6GQJCY22MSS2YEXUNAGM5R2GQCZH8K24CWRQML3NJSKM548ARUHPWSSQ9NVRVZ"; // Invoice without prefix assert!(matches!( - parse(bolt11, None).await?, + parse_with_rest_client(rest_client.as_ref(), bolt11, None).await?, InputType::Bolt11 { invoice: _invoice } )); // Invoice with prefix let invoice_with_prefix = format!("LIGHTNING:{bolt11}"); assert!(matches!( - parse(&invoice_with_prefix, None).await?, + parse_with_rest_client(rest_client.as_ref(), &invoice_with_prefix, None).await?, InputType::Bolt11 { invoice: _invoice } )); @@ -1141,6 +1165,8 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_bolt11_with_fallback_bitcoin_address() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); let addr = "1andreas3batLhQa2FawWjeyjCqyBzypd"; let bolt11 = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; @@ -1148,7 +1174,7 @@ pub(crate) mod tests { // BOLT11 is the first URI arg (preceded by '?') let addr_1 = format!("bitcoin:{addr}?lightning={bolt11}"); assert!(matches!( - parse(&addr_1, None).await?, + parse_with_rest_client(rest_client.as_ref(), &addr_1, None).await?, InputType::Bolt11 { invoice: _invoice } )); @@ -1156,7 +1182,7 @@ pub(crate) mod tests { // BOLT11 is not the first URI arg (preceded by '&') let addr_2 = format!("bitcoin:{addr}?amount=0.00002000&lightning={bolt11}"); assert!(matches!( - parse(&addr_2, None).await?, + parse_with_rest_client(rest_client.as_ref(), &addr_2, None).await?, InputType::Bolt11 { invoice: _invoice } )); @@ -1165,20 +1191,28 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_url() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); assert!(matches!( - parse("https://breez.technology", None).await?, + parse_with_rest_client(rest_client.as_ref(), "https://breez.technology", None).await?, InputType::Url { url: _url } )); assert!(matches!( - parse("https://breez.technology/", None).await?, + parse_with_rest_client(rest_client.as_ref(), "https://breez.technology/", None).await?, InputType::Url { url: _url } )); assert!(matches!( - parse("https://breez.technology/test-path", None).await?, + parse_with_rest_client( + rest_client.as_ref(), + "https://breez.technology/test-path", + None + ) + .await?, InputType::Url { url: _url } )); assert!(matches!( - parse( + parse_with_rest_client( + rest_client.as_ref(), "https://breez.technology/test-path?arg1=val1&arg2=val2", None ) @@ -1187,7 +1221,12 @@ pub(crate) mod tests { )); // `lightning` query param is not an LNURL. assert!(matches!( - parse("https://breez.technology?lightning=nonsense", None).await?, + parse_with_rest_client( + rest_client.as_ref(), + "https://breez.technology?lightning=nonsense", + None + ) + .await?, InputType::Url { url: _url } )); @@ -1196,11 +1235,17 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_node_id() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let secp = Secp256k1::new(); let secret_key = SecretKey::from_slice(&[0xab; 32])?; let public_key = PublicKey::from_secret_key(&secp, &secret_key); - match parse(&public_key.to_string(), None).await? { + mock_external_parser(&mock_rest_client, "".to_string(), 400); + mock_external_parser(&mock_rest_client, "".to_string(), 400); + mock_external_parser(&mock_rest_client, "".to_string(), 400); + let rest_client: Arc = Arc::new(mock_rest_client); + + match parse_with_rest_client(rest_client.as_ref(), &public_key.to_string(), None).await? { InputType::NodeId { node_id } => { assert_eq!(node_id, public_key.to_string()); } @@ -1208,36 +1253,49 @@ pub(crate) mod tests { } // Other formats and sizes - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "012345678901234567890123456789012345678901234567890123456789mnop", None ) .await .is_err()); - assert!(parse("0123456789", None).await.is_err()); - assert!(parse("abcdefghij", None).await.is_err()); + assert!( + parse_with_rest_client(rest_client.as_ref(), "0123456789", None) + .await + .is_err() + ); + assert!( + parse_with_rest_client(rest_client.as_ref(), "abcdefghij", None) + .await + .is_err() + ); // Plain Node ID - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", None ) .await .is_ok()); // Plain Node ID (66 hex chars) with @ separator and any string afterwards - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f@", None ) .await .is_ok()); - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f@sdfsffs", None ) .await .is_ok()); - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f@1.2.3.4:1234", None ) @@ -1245,19 +1303,22 @@ pub(crate) mod tests { .is_ok()); // Invalid Node ID (66 chars ending in non-hex-chars) with @ separator and any string afterwards -> invalid - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3zzz@", None ) .await .is_err()); - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3zzz@sdfsffs", None ) .await .is_err()); - assert!(parse( + assert!(parse_with_rest_client( + rest_client.as_ref(), "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3zzz@1.2.3.4:1234", None ) @@ -1329,38 +1390,35 @@ pub(crate) mod tests { Ok(()) } - async fn mock_lnurl_withdraw_endpoint(path: &str, return_lnurl_error: Option) { - let expected_lnurl_withdraw_data = r#" -{ - "tag":"withdrawRequest", - "callback":"https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538", - "k1":"37b4c919f871c090830cc47b92a544a30097f03430bc39670b8ec0da89f01a81", - "minWithdrawable":3000, - "maxWithdrawable":12000, - "defaultDescription":"sample withdraw" -} - "#.replace('\n', ""); - - let (response_body, status) = match &return_lnurl_error { - None => (expected_lnurl_withdraw_data, 200), - Some(err_reason) => ( - ["{\"status\": \"ERROR\", \"reason\": \"", err_reason, "\"}"].join(""), - 400, - ), + fn mock_lnurl_withdraw_endpoint(mock_rest_client: &MockRestClient, error: Option) { + let (response_body, status_code) = match error { + None => (json!({ + "tag": "withdrawRequest", + "callback": "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538", + "k1": "37b4c919f871c090830cc47b92a544a30097f03430bc39670b8ec0da89f01a81", + "minWithdrawable": 3000, + "maxWithdrawable": 12000, + "defaultDescription": "sample withdraw", + }).to_string(), 200), + Some(err_reason) => (json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), 400), }; - MOCK_HTTP_SERVER - .mock("GET", path, &response_body, Some(status), None, None) - .await; + mock_rest_client.add_response(MockResponse::new(status_code, response_body)); } #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_lud_03() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); // Covers cases in LUD-03: withdrawRequest base spec // https://github.com/lnurl/luds/blob/luds/03.md let path = "/lnurl-withdraw?session=bc893fafeb9819046781b47d68fdcf88fa39a28898784c183b42b7ac13820d81"; - mock_lnurl_withdraw_endpoint(path, None).await; + mock_lnurl_withdraw_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); let lnurl_withdraw_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; assert_eq!( @@ -1368,7 +1426,9 @@ pub(crate) mod tests { ("localhost".into(), format!("https://localhost{path}"), None,) ); - if let InputType::LnUrlWithdraw { data: wd } = parse(lnurl_withdraw_encoded, None).await? { + if let InputType::LnUrlWithdraw { data: wd } = + parse_with_rest_client(rest_client.as_ref(), lnurl_withdraw_encoded, None).await? + { assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); assert_eq!( wd.k1, @@ -1384,8 +1444,10 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_in_url() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); let path = "/lnurl-withdraw?session=bc893fafeb9819046781b47d68fdcf88fa39a28898784c183b42b7ac13820d81"; - mock_lnurl_withdraw_endpoint(path, None).await; + mock_lnurl_withdraw_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); let lnurl_withdraw_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; assert_eq!( @@ -1394,7 +1456,9 @@ pub(crate) mod tests { ); let url = format!("https://bitcoin.org?lightning={lnurl_withdraw_encoded}"); - if let InputType::LnUrlWithdraw { data: wd } = parse(&url, None).await? { + if let InputType::LnUrlWithdraw { data: wd } = + parse_with_rest_client(rest_client.as_ref(), &url, None).await? + { assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); assert_eq!( wd.k1, @@ -1410,6 +1474,8 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_auth_lud_04() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); // Covers cases in LUD-04: `auth` base spec // https://github.com/lnurl/luds/blob/luds/04.md @@ -1421,7 +1487,9 @@ pub(crate) mod tests { ("localhost".into(), decoded_url.into(), None) ); - if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded, None).await? { + if let InputType::LnUrlAuth { data: ad } = + parse_with_rest_client(rest_client.as_ref(), lnurl_auth_encoded, None).await? + { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1433,7 +1501,9 @@ pub(crate) mod tests { // Action = register let _decoded_url = "https://localhost/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822&action=register"; let lnurl_auth_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgezvctrw35k7m3awfjkw6tnw3jhys2umys"; - if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded, None).await? { + if let InputType::LnUrlAuth { data: ad } = + parse_with_rest_client(rest_client.as_ref(), lnurl_auth_encoded, None).await? + { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1445,7 +1515,9 @@ pub(crate) mod tests { // Action = login let _decoded_url = "https://localhost/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822&action=login"; let lnurl_auth_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgezvctrw35k7m3ad3hkw6tw2acjtx"; - if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded, None).await? { + if let InputType::LnUrlAuth { data: ad } = + parse_with_rest_client(rest_client.as_ref(), lnurl_auth_encoded, None).await? + { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1457,7 +1529,9 @@ pub(crate) mod tests { // Action = link let _decoded_url = "https://localhost/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822&action=link"; let lnurl_auth_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgezvctrw35k7m3ad35ku6cc8mvs6"; - if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded, None).await? { + if let InputType::LnUrlAuth { data: ad } = + parse_with_rest_client(rest_client.as_ref(), lnurl_auth_encoded, None).await? + { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1469,7 +1543,9 @@ pub(crate) mod tests { // Action = auth let _decoded_url = "https://localhost/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822&action=auth"; let lnurl_auth_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgezvctrw35k7m3av96hg6qmg6zgu"; - if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded, None).await? { + if let InputType::LnUrlAuth { data: ad } = + parse_with_rest_client(rest_client.as_ref(), lnurl_auth_encoded, None).await? + { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1481,107 +1557,70 @@ pub(crate) mod tests { // Action = another, invalid type let _decoded_url = "https://localhost/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822&action=invalid"; let lnurl_auth_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttvdankjm3lw3skw0tvdankjm3xdvcn6vtp8q6n2dfsx5mrjwtrxdjnqvtzv56rzcnyv3jrxv3sxqmkyenrvv6kve3exv6nqdtyv43nqcmzvdsnvdrzx33rsenxx5unqc3cxgezvctrw35k7m3ad9h8vctvd9jq2s4vfw"; - assert!(parse(lnurl_auth_encoded, None).await.is_err()); + assert!( + parse_with_rest_client(rest_client.as_ref(), lnurl_auth_encoded, None) + .await + .is_err() + ); Ok(()) } - async fn mock_lnurl_pay_endpoint(path: &str, return_lnurl_error: Option) { - let expected_lnurl_pay_data = r#" -{ - "callback":"https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7", - "tag":"payRequest", - "maxSendable":16000, - "minSendable":4000, - "metadata":"[ - [\"text/plain\",\"WRhtV\"], - [\"text/long-desc\",\"MBTrTiLCFS\"], - [\"image/png;base64\",\"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\"] - ]", - "commentAllowed":0, - "payerData":{ - "name":{"mandatory":false}, - "pubkey":{"mandatory":false}, - "identifier":{"mandatory":false}, - "email":{"mandatory":false}, - "auth":{"mandatory":false,"k1":"18ec6d5b96db6f219baed2f188aee7359fcf5bea11bb7d5b47157519474c2222"} - } -} - "#.replace('\n', ""); - - let response_body = match return_lnurl_error { - None => expected_lnurl_pay_data, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } - }; - - MOCK_HTTP_SERVER - .mock("GET", path, &response_body, None, None, None) - .await; - } - - async fn mock_lnurl_ln_address_endpoint(ln_address: &str, return_lnurl_error: Option) { - let (_domain, lnurl_pay_url, _ln_address) = ln_address_decode(ln_address).unwrap(); - let url = reqwest::Url::parse(&lnurl_pay_url).unwrap(); - let path = url.path(); - - let expected_lnurl_pay_data = r#" -{ - "callback":"https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7", - "tag":"payRequest", - "maxSendable":16000, - "minSendable":4000, - "metadata":"[ - [\"text/plain\",\"WRhtV\"], - [\"text/long-desc\",\"MBTrTiLCFS\"], - [\"image/png;base64\",\"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\"] - ]", - "commentAllowed":0, - "payerData":{ - "name":{"mandatory":false}, - "pubkey":{"mandatory":false}, - "identifier":{"mandatory":false}, - "email":{"mandatory":false}, - "auth":{"mandatory":false,"k1":"18ec6d5b96db6f219baed2f188aee7359fcf5bea11bb7d5b47157519474c2222"} - } -} - "#.replace('\n', ""); - - let response_body = match return_lnurl_error { - None => expected_lnurl_pay_data, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + fn mock_lnurl_pay_endpoint(mock_rest_client: &MockRestClient, error: Option) { + let response_body = match error { + None => json!({ + "callback":"https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7", + "tag": "payRequest", + "maxSendable": 16000, + "minSendable": 4000, + "metadata": "[ + [\"text/plain\",\"WRhtV\"], + [\"text/long-desc\",\"MBTrTiLCFS\"], + [\"image/png;base64\",\"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\"] + ]", + "commentAllowed": 0, + "payerData":{ + "name": { "mandatory":false }, + "pubkey": { "mandatory":false }, + "identifier": { "mandatory":false }, + "email":{ "mandatory":false }, + "auth": { "mandatory":false, "k1":"18ec6d5b96db6f219baed2f188aee7359fcf5bea11bb7d5b47157519474c2222" } + } + }).to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - MOCK_HTTP_SERVER - .mock( - "GET", - path, - &response_body, - None, - None, - Some(&lnurl_pay_url), - ) - .await; + mock_rest_client.add_response(MockResponse::new(200, response_body)); } #[sdk_macros::async_test_all] async fn test_lnurl_pay_lud_06() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); // Covers cases in LUD-06: payRequest base spec // https://github.com/lnurl/luds/blob/luds/06.md let lnurl_pay_encoded = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf"; let path = "/lnurl-pay?session=db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"; - mock_lnurl_pay_endpoint(path, None).await; + mock_lnurl_pay_endpoint(&mock_rest_client, None); + mock_lnurl_pay_endpoint(&mock_rest_client, None); + mock_lnurl_pay_endpoint(&mock_rest_client, None); + mock_lnurl_pay_endpoint(&mock_rest_client, None); + mock_lnurl_pay_endpoint(&mock_rest_client, None); + mock_lnurl_pay_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); assert_eq!( lnurl_decode(lnurl_pay_encoded)?, ("localhost".into(), format!("https://localhost{path}"), None) ); - if let InputType::LnUrlPay { data: pd, .. } = parse(lnurl_pay_encoded, None).await? { + if let InputType::LnUrlPay { data: pd, .. } = + parse_with_rest_client(rest_client.as_ref(), lnurl_pay_encoded, None).await? + { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1612,7 +1651,6 @@ pub(crate) mod tests { } for lnurl_pay in [ - lnurl_pay_encoded, lnurl_pay_encoded.to_uppercase().as_str(), format!("lightning:{}", lnurl_pay_encoded).as_str(), format!("lightning:{}", lnurl_pay_encoded.to_uppercase()).as_str(), @@ -1620,7 +1658,7 @@ pub(crate) mod tests { format!("LIGHTNING:{}", lnurl_pay_encoded.to_uppercase()).as_str(), ] { assert!(matches!( - parse(lnurl_pay, None).await?, + parse_with_rest_client(rest_client.as_ref(), lnurl_pay, None).await?, InputType::LnUrlPay { .. } )); } @@ -1629,13 +1667,17 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_pay_lud_16_ln_address() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); // Covers cases in LUD-16: Paying to static internet identifiers (LN Address) // https://github.com/lnurl/luds/blob/luds/16.md let ln_address = "user@domain.net"; - mock_lnurl_ln_address_endpoint(ln_address, None).await; + mock_lnurl_pay_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); - if let InputType::LnUrlPay { data: pd, .. } = parse(ln_address, None).await? { + if let InputType::LnUrlPay { data: pd, .. } = + parse_with_rest_client(rest_client.as_ref(), ln_address, None).await? + { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1672,13 +1714,17 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_pay_lud_16_ln_address_with_prefix() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); // Covers cases in LUD-16, with BIP-353 prefix. let ln_address = "â‚¿user@domain.net"; let server_ln_address = "user@domain.net"; - mock_lnurl_ln_address_endpoint(server_ln_address, None).await; + mock_lnurl_pay_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); - if let InputType::LnUrlPay { data: pd, .. } = parse(ln_address, None).await? { + if let InputType::LnUrlPay { data: pd, .. } = + parse_with_rest_client(rest_client.as_ref(), ln_address, None).await? + { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1694,14 +1740,18 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_pay_lud_16_ln_address_error() -> Result<()> { + let mock_rest_client = MockRestClient::new(); // Covers cases in LUD-16: Paying to static internet identifiers (LN Address) // https://github.com/lnurl/luds/blob/luds/16.md let ln_address = "error@domain.com"; let expected_err = "Error msg from LNURL endpoint found via LN Address"; - mock_lnurl_ln_address_endpoint(ln_address, Some(expected_err.to_string())).await; + mock_lnurl_pay_endpoint(&mock_rest_client, Some(expected_err.to_string())); + let rest_client: Arc = Arc::new(mock_rest_client); - if let InputType::LnUrlError { data: msg } = parse(ln_address, None).await? { + if let InputType::LnUrlError { data: msg } = + parse_with_rest_client(rest_client.as_ref(), ln_address, None).await? + { assert_eq!(msg.reason, expected_err); return Ok(()); } @@ -1879,12 +1929,16 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_pay_lud_17() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); let pay_path = "/lnurl-pay?session=db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"; - mock_lnurl_pay_endpoint(pay_path, None).await; + mock_lnurl_pay_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); let lnurl_pay_url = format!("lnurlp://localhost{pay_path}"); - if let InputType::LnUrlPay { data: pd, .. } = parse(&lnurl_pay_url, None).await? { + if let InputType::LnUrlPay { data: pd, .. } = + parse_with_rest_client(rest_client.as_ref(), &lnurl_pay_url, None).await? + { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1919,11 +1973,17 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_lud_17() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); let withdraw_path = "/lnurl-withdraw?session=e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"; - mock_lnurl_withdraw_endpoint(withdraw_path, None).await; + mock_lnurl_withdraw_endpoint(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); - if let InputType::LnUrlWithdraw { data: wd } = - parse(&format!("lnurlw://localhost{withdraw_path}"), None).await? + if let InputType::LnUrlWithdraw { data: wd } = parse_with_rest_client( + rest_client.as_ref(), + &format!("lnurlw://localhost{withdraw_path}"), + None, + ) + .await? { assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); assert_eq!( @@ -1940,10 +2000,16 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_auth_lud_17() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); let auth_path = "/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822"; - if let InputType::LnUrlAuth { data: ad } = - parse(&format!("keyauth://localhost{auth_path}"), None).await? + if let InputType::LnUrlAuth { data: ad } = parse_with_rest_client( + rest_client.as_ref(), + &format!("keyauth://localhost{auth_path}"), + None, + ) + .await? { assert_eq!( ad.k1, @@ -1956,12 +2022,18 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_pay_lud_17_error() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let pay_path = "/lnurl-pay?session=paylud17error"; let expected_error_msg = "test pay error"; - mock_lnurl_pay_endpoint(pay_path, Some(expected_error_msg.to_string())).await; + mock_lnurl_pay_endpoint(&mock_rest_client, Some(expected_error_msg.to_string())); + let rest_client: Arc = Arc::new(mock_rest_client); - if let InputType::LnUrlError { data: msg } = - parse(&format!("lnurlp://localhost{pay_path}"), None).await? + if let InputType::LnUrlError { data: msg } = parse_with_rest_client( + rest_client.as_ref(), + &format!("lnurlp://localhost{pay_path}"), + None, + ) + .await? { assert_eq!(msg.reason, expected_error_msg); return Ok(()); @@ -1972,12 +2044,18 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_lud_17_error() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let withdraw_path = "/lnurl-withdraw?session=withdrawlud17error"; let expected_error_msg = "test withdraw error"; - mock_lnurl_withdraw_endpoint(withdraw_path, Some(expected_error_msg.to_string())).await; + mock_lnurl_withdraw_endpoint(&mock_rest_client, Some(expected_error_msg.to_string())); + let rest_client: Arc = Arc::new(mock_rest_client); - if let InputType::LnUrlError { data: msg } = - parse(&format!("lnurlw://localhost{withdraw_path}"), None).await? + if let InputType::LnUrlError { data: msg } = parse_with_rest_client( + rest_client.as_ref(), + &format!("lnurlw://localhost{withdraw_path}"), + None, + ) + .await? { assert_eq!(msg.reason, expected_error_msg); return Ok(()); @@ -1986,36 +2064,30 @@ pub(crate) mod tests { Err(anyhow!("Unrecognized input type")) } - async fn mock_external_parser(path: &str, response: &str, status: usize) { - MOCK_HTTP_SERVER - .mock( - "GET", - path, - response, - Some(status), - None, - Some(&format!("http://127.0.0.1:8080{path}")), - ) - .await; + fn mock_external_parser( + mock_rest_client: &MockRestClient, + response_body: String, + status_code: u16, + ) { + mock_rest_client.add_response(MockResponse::new(status_code, response_body)); } #[sdk_macros::async_test_all] async fn test_external_parsing_lnurlp_first_response() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); let input = "123provider.domain32/1"; - let path = format!( - "/{}", - percent_encoding::utf8_percent_encode(input, NON_ALPHANUMERIC) - ); - let response = r#" + let response = json!( { - "callback":"callback_url", - "minSendable":57000, - "maxSendable":57000, - "metadata":"[[\"text/plain\", \"External payment\"]]","tag":"payRequest" - } - "#; - mock_external_parser(&path, response, 200).await; + "callback": "callback_url", + "minSendable": 57000, + "maxSendable": 57000, + "metadata": "[[\"text/plain\", \"External payment\"]]", + "tag": "payRequest" + }) + .to_string(); + mock_external_parser(&mock_rest_client, response, 200); + let rest_client: Arc = Arc::new(mock_rest_client); let parsers = vec![ExternalInputParser { provider_id: "id".to_string(), @@ -2023,7 +2095,8 @@ pub(crate) mod tests { parser_url: "http://127.0.0.1:8080/".to_string(), }]; - let input_type = parse(input, Some(&parsers)).await?; + let input_type = + parse_with_rest_client(rest_client.as_ref(), input, Some(&parsers)).await?; if let InputType::LnUrlPay { data, .. } = input_type { assert_eq!(data.callback, "callback_url"); assert_eq!(data.max_sendable, 57000); @@ -2049,23 +2122,17 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_external_parsing_bitcoin_address_and_bolt11( ) -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); // Bitcoin parsing endpoint let bitcoin_input = "123bitcoin.address.provider32/1"; - let path = format!( - "/{}", - percent_encoding::utf8_percent_encode(bitcoin_input, NON_ALPHANUMERIC) - ); - let bitcoin_address = "1andreas3batLhQa2FawWjeyjCqyBzypd"; - mock_external_parser(&path, bitcoin_address, 200).await; + let bitcoin_address = "1andreas3batLhQa2FawWjeyjCqyBzypd".to_string(); + mock_external_parser(&mock_rest_client, bitcoin_address.clone(), 200); // Bolt11 parsing endpoint let bolt11_input = "123bolt11.provider32/1"; - let path = format!( - "/{}", - percent_encoding::utf8_percent_encode(bolt11_input, NON_ALPHANUMERIC) - ); - let bolt11 = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; - mock_external_parser(&path, bolt11, 200).await; + let bolt11 = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz".to_string(); + mock_external_parser(&mock_rest_client, bolt11.clone(), 200); + let rest_client: Arc = Arc::new(mock_rest_client); // Set parsers let parsers = vec![ @@ -2082,14 +2149,16 @@ pub(crate) mod tests { ]; // Parse and check results - let input_type = parse(bitcoin_input, Some(&parsers)).await?; + let input_type = + parse_with_rest_client(rest_client.as_ref(), bitcoin_input, Some(&parsers)).await?; if let InputType::BitcoinAddress { address } = input_type { assert_eq!(address.address, bitcoin_address); } else { panic!("Expected BitcoinAddress, got {:?}", input_type); } - let input_type = parse(bolt11_input, Some(&parsers)).await?; + let input_type = + parse_with_rest_client(rest_client.as_ref(), bolt11_input, Some(&parsers)).await?; if let InputType::Bolt11 { invoice } = input_type { assert_eq!(invoice.bolt11, bolt11); } else { @@ -2101,13 +2170,11 @@ pub(crate) mod tests { #[sdk_macros::async_test_all] async fn test_external_parsing_error() -> Result<(), Box> { + let mock_rest_client = MockRestClient::new(); let input = "123provider.domain.error32/1"; - let path = format!( - "/{}", - percent_encoding::utf8_percent_encode(input, NON_ALPHANUMERIC) - ); - let response = "Unrecognized input"; - mock_external_parser(&path, response, 400).await; + let response = "Unrecognized input".to_string(); + mock_external_parser(&mock_rest_client, response, 400); + let rest_client: Arc = Arc::new(mock_rest_client); let parsers = vec![ExternalInputParser { provider_id: "id".to_string(), @@ -2115,7 +2182,7 @@ pub(crate) mod tests { parser_url: "http://127.0.0.1:8080/".to_string(), }]; - let result = parse(input, Some(&parsers)).await; + let result = parse_with_rest_client(rest_client.as_ref(), input, Some(&parsers)).await; assert!(matches!(result, Err(e) if e.to_string() == "Unrecognized input type")); diff --git a/libs/sdk-common/src/lib.rs b/libs/sdk-common/src/lib.rs index c8cac90bd..df45e344e 100644 --- a/libs/sdk-common/src/lib.rs +++ b/libs/sdk-common/src/lib.rs @@ -48,5 +48,7 @@ pub mod prelude { pub use crate::lnurl::specs::withdraw::*; pub use crate::lnurl::*; pub use crate::model::*; + #[cfg(feature = "test-utils")] + pub use crate::test_utils::mock_rest_client::*; pub use crate::utils::rest_client::*; } diff --git a/libs/sdk-common/src/liquid/mod.rs b/libs/sdk-common/src/liquid/mod.rs index 59266825b..0ca410c2a 100644 --- a/libs/sdk-common/src/liquid/mod.rs +++ b/libs/sdk-common/src/liquid/mod.rs @@ -3,22 +3,27 @@ pub use bip21::*; #[cfg(test)] mod tests { + use std::sync::Arc; + use anyhow::{anyhow, Result}; use elements::AssetId; use crate::input_parser::tests::get_bip21_rounding_test_vectors; use crate::prelude::*; + use crate::test_utils::mock_rest_client::MockRestClient; #[cfg(all(target_family = "wasm", target_os = "unknown"))] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[sdk_macros::async_test_all] async fn test_liquid_address_bip21_rounding() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); let asset_id = AssetId::LIQUID_BTC.to_string(); for (amount_sat, amount_btc) in get_bip21_rounding_test_vectors() { let addr = format!("liquidnetwork:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk?amount={amount_btc}&assetid={asset_id}"); - match parse(&addr, None).await? { + match parse_with_rest_client(rest_client.as_ref(), &addr, None).await? { InputType::LiquidAddress { address: addr_with_amount_parsed, } => { diff --git a/libs/sdk-common/src/lnurl/error.rs b/libs/sdk-common/src/lnurl/error.rs index 9ccaac8c5..55ddd3cec 100644 --- a/libs/sdk-common/src/lnurl/error.rs +++ b/libs/sdk-common/src/lnurl/error.rs @@ -2,7 +2,7 @@ use std::{array::TryFromSliceError, string::FromUtf8Error}; use bitcoin::{bech32, secp256k1, util::bip32}; -use crate::prelude::InvoiceError; +use crate::prelude::{InvoiceError, ServiceConnectivityError}; pub type LnUrlResult = Result; @@ -79,6 +79,12 @@ impl From for LnUrlError { } } +impl From for LnUrlError { + fn from(value: ServiceConnectivityError) -> Self { + LnUrlError::ServiceConnectivity(value.err) + } +} + impl From for LnUrlError { fn from(err: TryFromSliceError) -> Self { Self::Generic(err.to_string()) diff --git a/libs/sdk-common/src/lnurl/mod.rs b/libs/sdk-common/src/lnurl/mod.rs index 3a441168d..ca52e6a66 100644 --- a/libs/sdk-common/src/lnurl/mod.rs +++ b/libs/sdk-common/src/lnurl/mod.rs @@ -2,38 +2,6 @@ pub mod error; pub mod model; pub mod specs; -use super::prelude::*; - -/// Replaces the scheme, host and port with a local mockito host. Preserves the rest of the path. -#[cfg(test)] -pub(crate) fn maybe_replace_host_with_mock_test_host( - lnurl_endpoint: String, -) -> LnUrlResult { - // During tests, the mockito test URL chooses a free port. This cannot be known in advance, - // so the URL has to be adjusted dynamically. - let url = crate::input_parser::tests::MOCK_HTTP_SERVER.url(lnurl_endpoint.clone()); - let mock_endpoint_url = - reqwest::Url::parse(&url).map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - let mut parsed_lnurl_endpoint = - reqwest::Url::parse(&lnurl_endpoint).map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - - parsed_lnurl_endpoint - .set_host(mock_endpoint_url.host_str()) - .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - let _ = parsed_lnurl_endpoint.set_scheme(mock_endpoint_url.scheme()); - let _ = parsed_lnurl_endpoint.set_port(mock_endpoint_url.port()); - - Ok(parsed_lnurl_endpoint.to_string()) -} - -#[cfg(not(test))] -pub(crate) fn maybe_replace_host_with_mock_test_host( - lnurl_endpoint: String, -) -> LnUrlResult { - // When not called from a test, we fallback to keeping the URL intact - Ok(lnurl_endpoint) -} - #[cfg(test)] mod tests { use bitcoin::secp256k1::rand; diff --git a/libs/sdk-common/src/lnurl/specs/auth.rs b/libs/sdk-common/src/lnurl/specs/auth.rs index 4950b0343..dcc1de891 100644 --- a/libs/sdk-common/src/lnurl/specs/auth.rs +++ b/libs/sdk-common/src/lnurl/specs/auth.rs @@ -25,7 +25,8 @@ pub trait LnurlAuthSigner { /// https://github.com/lnurl/luds/blob/luds/05.md /// /// See the [parse] docs for more detail on the full workflow. -pub async fn perform_lnurl_auth( +pub async fn perform_lnurl_auth( + rest_client: &C, req_data: &LnUrlAuthRequestData, signer: &S, ) -> LnUrlResult { @@ -50,10 +51,8 @@ pub async fn perform_lnurl_auth( callback_url .query_pairs_mut() .append_pair("key", &xpub.public_key.to_hex()); - - get_parse_and_log_response(callback_url.as_ref(), false) - .await - .map_err(|e| LnUrlError::ServiceConnectivity(e.to_string())) + let (response, _) = rest_client.get(callback_url.as_ref()).await?; + Ok(parse_json(&response)?) } pub fn validate_request( diff --git a/libs/sdk-common/src/lnurl/specs/pay.rs b/libs/sdk-common/src/lnurl/specs/pay.rs index a62ef8708..013444454 100644 --- a/libs/sdk-common/src/lnurl/specs/pay.rs +++ b/libs/sdk-common/src/lnurl/specs/pay.rs @@ -9,7 +9,8 @@ pub type Aes256CbcDec = cbc::Decryptor; /// /// /// See the [parse] docs for more detail on the full workflow. -pub async fn validate_lnurl_pay( +pub async fn validate_lnurl_pay( + rest_client: &C, user_amount_msat: u64, comment: &Option, req_data: &LnUrlPayRequestData, @@ -25,14 +26,11 @@ pub async fn validate_lnurl_pay( )?; let callback_url = build_pay_callback_url(user_amount_msat, comment, req_data)?; - let (callback_resp_text, _) = get_and_log_response(&callback_url) - .await - .map_err(|e| LnUrlError::ServiceConnectivity(e.to_string()))?; - - if let Ok(err) = serde_json::from_str::(&callback_resp_text) { + let (response, _) = rest_client.get(&callback_url).await?; + if let Ok(err) = serde_json::from_str::(&response) { Ok(ValidatedCallbackResponse::EndpointError { data: err }) } else { - let mut callback_resp: CallbackResponse = serde_json::from_str(&callback_resp_text)?; + let mut callback_resp: CallbackResponse = serde_json::from_str(&response)?; if let Some(ref sa) = callback_resp.success_action { match sa { SuccessAction::Aes { data } => data.validate()?, diff --git a/libs/sdk-common/src/lnurl/specs/withdraw.rs b/libs/sdk-common/src/lnurl/specs/withdraw.rs index 513b49cbe..4e68beeca 100644 --- a/libs/sdk-common/src/lnurl/specs/withdraw.rs +++ b/libs/sdk-common/src/lnurl/specs/withdraw.rs @@ -11,7 +11,8 @@ use crate::prelude::*; /// Note that the invoice amount has to respect two separate min/max limits: /// * those in the [LnUrlWithdrawRequestData] showing the limits of the LNURL endpoint, and /// * those of the current node, depending on the LSP settings and LN channel conditions -pub async fn validate_lnurl_withdraw( +pub async fn validate_lnurl_withdraw( + rest_client: &C, req_data: LnUrlWithdrawRequestData, invoice: LNInvoice, ) -> LnUrlResult { @@ -34,9 +35,12 @@ pub async fn validate_lnurl_withdraw( // Send invoice to the LNURL-w endpoint via the callback let callback_url = build_withdraw_callback_url(&req_data, &invoice)?; - let callback_url = maybe_replace_host_with_mock_test_host(callback_url)?; - let withdraw_status = match get_parse_and_log_response(&callback_url, false).await { + let response = rest_client + .get(&callback_url) + .await + .and_then(|(response, _)| parse_json(&response)); + let withdraw_status = match response { Ok(LnUrlCallbackStatus::Ok) => LnUrlWithdrawResult::Ok { data: LnUrlWithdrawSuccessData { invoice }, }, @@ -181,61 +185,49 @@ pub mod model { #[cfg(test)] mod tests { + use std::sync::Arc; + use anyhow::Result; + use serde_json::json; - use crate::input_parser::tests::MOCK_HTTP_SERVER; use crate::lnurl::tests::rand_string; use crate::prelude::*; + use crate::test_utils::mock_rest_client::{MockResponse, MockRestClient}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_validate_amount_failure() -> Result<()> { + let mock_rest_client = MockRestClient::new(); + let rest_client: Arc = Arc::new(mock_rest_client); + let invoice_str = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; let invoice = crate::invoice::parse_invoice(invoice_str)?; let withdraw_req = get_test_withdraw_req_data(0, 1); // Fail validation before even calling the endpoint (no mock needed) - assert!(validate_lnurl_withdraw(withdraw_req, invoice) - .await - .is_err()); + assert!( + validate_lnurl_withdraw(rest_client.as_ref(), withdraw_req, invoice) + .await + .is_err() + ); Ok(()) } /// Mock an LNURL-withdraw endpoint that responds with an OK to a withdraw attempt - async fn mock_lnurl_withdraw_callback( - withdraw_req: &LnUrlWithdrawRequestData, - invoice: &LNInvoice, - error: Option, - ) { - let callback_url = build_withdraw_callback_url(withdraw_req, invoice).unwrap(); - let url = reqwest::Url::parse(&callback_url).unwrap(); - let mockito_path: &str = &format!("{}?{}", url.path(), url.query().unwrap()); - - let expected_payload = r#" - {"status": "OK"} - "# - .replace('\n', ""); - + fn mock_lnurl_withdraw_callback(mock_rest_client: &MockRestClient, error: Option) { let response_body = match error { - None => expected_payload, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + None => json!({"status": "OK"}).to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - let _url = MOCK_HTTP_SERVER - .mock( - "GET", - mockito_path, - &response_body, - None, - None, - Some(&callback_url), - ) - .await; + mock_rest_client.add_response(MockResponse::new(200, response_body)); } fn get_test_withdraw_req_data(min_sat: u64, max_sat: u64) -> LnUrlWithdrawRequestData { @@ -250,14 +242,16 @@ mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_success() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let invoice_str = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; let req_invoice = crate::invoice::parse_invoice(invoice_str)?; let withdraw_req = get_test_withdraw_req_data(0, 100); - mock_lnurl_withdraw_callback(&withdraw_req, &req_invoice, None).await; + mock_lnurl_withdraw_callback(&mock_rest_client, None); + let rest_client: Arc = Arc::new(mock_rest_client); assert!(matches!( - validate_lnurl_withdraw(withdraw_req, req_invoice.clone()).await?, + validate_lnurl_withdraw(rest_client.as_ref(), withdraw_req, req_invoice.clone()).await?, LnUrlWithdrawResult::Ok { data: LnUrlWithdrawSuccessData { invoice } } if invoice == req_invoice )); @@ -266,15 +260,17 @@ mod tests { #[sdk_macros::async_test_all] async fn test_lnurl_withdraw_endpoint_failure() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let invoice_str = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; let invoice = crate::invoice::parse_invoice(invoice_str)?; let withdraw_req = get_test_withdraw_req_data(0, 100); // Generic error reported by endpoint - mock_lnurl_withdraw_callback(&withdraw_req, &invoice, Some("error".parse()?)).await; + mock_lnurl_withdraw_callback(&mock_rest_client, Some("error".to_string())); + let rest_client: Arc = Arc::new(mock_rest_client); assert!(matches!( - validate_lnurl_withdraw(withdraw_req, invoice).await?, + validate_lnurl_withdraw(rest_client.as_ref(), withdraw_req, invoice).await?, LnUrlWithdrawResult::ErrorStatus { data: _ } )); diff --git a/libs/sdk-common/src/test_utils/mock_rest_client.rs b/libs/sdk-common/src/test_utils/mock_rest_client.rs new file mode 100644 index 000000000..d330112d1 --- /dev/null +++ b/libs/sdk-common/src/test_utils/mock_rest_client.rs @@ -0,0 +1,64 @@ +use std::{ + collections::{HashMap, VecDeque}, + sync::Mutex, +}; + +use crate::{error::ServiceConnectivityError, prelude::RestClient}; + +#[derive(Debug)] +pub struct MockResponse { + pub(crate) status_code: u16, + pub(crate) text: String, +} + +impl MockResponse { + pub fn new(status_code: u16, text: String) -> Self { + MockResponse { status_code, text } + } +} + +#[derive(Default)] +pub struct MockRestClient { + responses: Mutex>, +} + +impl MockRestClient { + pub fn new() -> Self { + MockRestClient::default() + } + + pub fn add_response(&self, response: MockResponse) -> &Self { + println!("Push response: {response:?}"); + let mut responses = self.responses.lock().unwrap(); + responses.push_back(response); + self + } +} + +#[sdk_macros::async_trait] +impl RestClient for MockRestClient { + async fn get(&self, _url: &str) -> Result<(String, u16), ServiceConnectivityError> { + let mut responses = self.responses.lock().unwrap(); + let response = responses.pop_front().unwrap(); + println!("Pop GET response: {response:?}"); + let status = response.status_code; + let raw_body = response.text; + + Ok((raw_body, status)) + } + + async fn post( + &self, + _url: &str, + _headers: Option>, + _body: Option, + ) -> Result<(String, u16), ServiceConnectivityError> { + let mut responses = self.responses.lock().unwrap(); + let response = responses.pop_front().unwrap(); + println!("Pop POST response: {response:?}"); + let status = response.status_code; + let raw_body = response.text; + + Ok((raw_body, status)) + } +} diff --git a/libs/sdk-common/src/test_utils/mock_server.rs b/libs/sdk-common/src/test_utils/mock_server.rs deleted file mode 100644 index 226f53d1c..000000000 --- a/libs/sdk-common/src/test_utils/mock_server.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::sync::Mutex; - -pub(crate) struct MockServer { - server: Mutex, -} - -pub(crate) struct ServerOpts { - pub host: &'static str, - pub port: u16, -} - -impl MockServer { - pub fn new_with_opts(opts: ServerOpts) -> Self { - Self { - server: Mutex::new(mockito::Server::new_with_opts(mockito::ServerOpts { - host: opts.host, - port: opts.port, - ..Default::default() - })), - } - } - - pub fn url(&self, _url: String) -> String { - self.server.lock().unwrap().url() - } - - pub async fn mock( - &self, - method: &str, - path: &str, - body: &str, - status_code: Option, - content_type: Option<&str>, - _url: Option<&str>, - ) -> String { - let mut server = self.server.lock().unwrap(); - let mut mock = server.mock(method, path).with_body(body); - if let Some(status_code) = status_code { - mock = mock.with_status(status_code); - } - if let Some(content_type) = content_type { - mock = mock.with_header("Content-Type", content_type); - } - mock.create(); - server.url() - } -} diff --git a/libs/sdk-common/src/test_utils/mock_server_wasm.rs b/libs/sdk-common/src/test_utils/mock_server_wasm.rs deleted file mode 100644 index 2384ee2e6..000000000 --- a/libs/sdk-common/src/test_utils/mock_server_wasm.rs +++ /dev/null @@ -1,101 +0,0 @@ -use js_sys::*; -use reqwest::Client; -use serde::Serialize; -use std::collections::HashMap; -use std::sync::Mutex; -use wasm_bindgen::{prelude::*, JsCast}; -use wasm_bindgen_futures::JsFuture; -use web_sys::*; - -#[wasm_bindgen(module = "/mock_service_worker.js")] -extern "C" {} - -#[derive(Debug, Serialize)] -struct MockConfig { - pub kind: String, - pub method: String, - pub path: String, - pub status_code: u16, - pub content_type: Option, - pub body: Vec, -} - -pub(crate) struct MockServer { - client: Mutex, - url_map: Mutex>, -} - -#[allow(dead_code)] -pub(crate) struct ServerOpts { - pub host: &'static str, - pub port: u16, -} - -impl MockServer { - pub fn new_with_opts(_opts: ServerOpts) -> Self { - Self { - client: Mutex::new(Client::new()), - url_map: Mutex::new(HashMap::new()), - } - } - - pub fn url(&self, url: String) -> String { - self.url_map - .lock() - .unwrap() - .get(&url) - .cloned() - .unwrap_or(url) - } - - pub async fn mock( - &self, - method: &str, - path: &str, - body: &str, - status_code: Option, - content_type: Option<&str>, - url: Option<&str>, - ) -> String { - let swc = window().unwrap().navigator().service_worker(); - let registration: ServiceWorkerRegistration = - JsFuture::from(swc.register("/mock_service_worker.js")) - .await - .unwrap() - .unchecked_into(); - JsFuture::from(swc.ready().unwrap()).await.unwrap(); - let sw = registration.active().unwrap(); - let mut nonce = [0; 16]; - getrandom::getrandom(&mut nonce).unwrap(); - let nonce = hex::encode(nonce); - let config = MockConfig { - kind: "config".into(), - method: method.into(), - path: path.into(), - status_code: status_code.unwrap_or(200) as u16, - body: body.into(), - content_type: content_type.map(|c| c.into()), - }; - if sw.state() == ServiceWorkerState::Activating { - JsFuture::from(Promise::new(&mut |rs, _| sw.set_onstatechange(Some(&rs)))) - .await - .unwrap(); - } - let client = self.client.lock().unwrap(); - client - .post(&format!("http://mock_configure/{nonce}")) - .json(&config) - .send() - .await - .unwrap() - .error_for_status() - .unwrap(); - let mock_url = format!("http://mock_{}/", nonce); - let mocked_url = url - .unwrap_or(&format!("https://localhost{path}")) - .to_string(); - let mut url_map = self.url_map.lock().unwrap(); - url_map.insert(mocked_url, mock_url.clone()); - mock_url - } -} diff --git a/libs/sdk-common/src/test_utils/mod.rs b/libs/sdk-common/src/test_utils/mod.rs index a96b1dd6d..e3969a4cd 100644 --- a/libs/sdk-common/src/test_utils/mod.rs +++ b/libs/sdk-common/src/test_utils/mod.rs @@ -1,7 +1,2 @@ -#![cfg(test)] - -#[cfg_attr( - all(target_family = "wasm", target_os = "unknown"), - path = "mock_server_wasm.rs" -)] -pub(crate) mod mock_server; +#[allow(dead_code)] +pub mod mock_rest_client; diff --git a/libs/sdk-common/src/utils/rest_client.rs b/libs/sdk-common/src/utils/rest_client.rs index 6da762085..3a7e0474b 100644 --- a/libs/sdk-common/src/utils/rest_client.rs +++ b/libs/sdk-common/src/utils/rest_client.rs @@ -1,75 +1,98 @@ use log::*; -use reqwest::StatusCode; -use std::time::Duration; +use reqwest::Client; +use std::{collections::HashMap, time::Duration}; use crate::error::{ServiceConnectivityError, ServiceConnectivityErrorKind}; const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); -/// Creates an HTTP client with a built-in connection timeout -pub fn get_reqwest_client() -> Result { - reqwest::Client::builder().build().map_err(Into::into) -} +#[sdk_macros::async_trait] +pub trait RestClient: Send + Sync { + /// Makes a GET request and logs on DEBUG. + /// ### Arguments + /// - `url`: the URL on which GET will be called + async fn get(&self, url: &str) -> Result<(String, u16), ServiceConnectivityError>; -pub async fn post_and_log_response( - url: &str, - body: Option, -) -> Result { - debug!("Making POST request to: {url}"); + /// Makes a POST request, and logs on DEBUG. + /// ### Arguments + /// - `url`: the URL on which POST will be called + /// - `headers`: the optional POST headers + /// - `body`: the optional POST body + async fn post( + &self, + url: &str, + headers: Option>, + body: Option, + ) -> Result<(String, u16), ServiceConnectivityError>; +} - let mut req = get_reqwest_client()?.post(url).timeout(REQUEST_TIMEOUT); - if let Some(body) = body { - req = req.body(body); +pub struct ReqwestRestClient { + client: Client, +} +impl ReqwestRestClient { + pub fn new() -> Result { + let client = Client::builder() + .build() + .map_err(Into::::into)?; + Ok(ReqwestRestClient { client }) } - let response = req.send().await?; - let status = response.status(); - let raw_body = response.text().await?; - debug!("Received response, status: {status}"); - trace!("raw response body: {raw_body}"); - - Ok(raw_body) } -/// Makes a GET request to the specified `url` and logs on DEBUG: -/// - the URL -/// - the raw response body -/// - the response HTTP status code -pub async fn get_and_log_response( - url: &str, -) -> Result<(String, StatusCode), ServiceConnectivityError> { - debug!("Making GET request to: {url}"); +#[sdk_macros::async_trait] +impl RestClient for ReqwestRestClient { + async fn get(&self, url: &str) -> Result<(String, u16), ServiceConnectivityError> { + debug!("Making GET request to: {url}"); + let response = self.client.get(url).timeout(REQUEST_TIMEOUT).send().await?; + let status = response.status().into(); + let raw_body = response.text().await?; + debug!("Received response, status: {status}"); + trace!("raw response body: {raw_body}"); - let response = get_reqwest_client()? - .get(url) - .timeout(REQUEST_TIMEOUT) - .send() - .await?; - let status = response.status(); - let raw_body = response.text().await?; - debug!("Received response, status: {status}"); - trace!("raw response body: {raw_body}"); + Ok((raw_body, status)) + } - Ok((raw_body, status)) + async fn post( + &self, + url: &str, + headers: Option>, + body: Option, + ) -> Result<(String, u16), ServiceConnectivityError> { + debug!("Making POST request to: {url}"); + let mut req = self.client.post(url).timeout(REQUEST_TIMEOUT); + if let Some(headers) = headers { + for (key, value) in headers.iter() { + req = req.header(key, value); + } + } + if let Some(body) = body { + req = req.body(body); + } + let response = req.send().await?; + let status = response.status(); + let raw_body = response.text().await?; + debug!("Received response, status: {status}"); + trace!("raw response body: {raw_body}"); + + Ok((raw_body, status.into())) + } } -/// Wrapper around [get_and_log_response] that, in addition, parses the payload into an expected type. -/// -/// ### Arguments -/// -/// - `url`: the URL on which GET will be called -/// - `enforce_status_check`: if true, the HTTP status code is checked in addition to trying to -/// parse the payload. In this case, an HTTP error code will automatically cause this function to -/// return `Err`, regardless of the payload. If false, the result type will be determined only -/// by the result of parsing the payload into the desired target type. -pub async fn get_parse_and_log_response( - url: &str, - enforce_status_check: bool, -) -> Result +pub fn parse_json(json: &str) -> Result where for<'a> T: serde::de::Deserialize<'a>, { - let (raw_body, status) = get_and_log_response(url).await?; - if enforce_status_check && !status.is_success() { + serde_json::from_str::(json).map_err(|e| { + ServiceConnectivityError::new(ServiceConnectivityErrorKind::Json, e.to_string()) + }) +} + +pub async fn get_and_check_success( + rest_client: &C, + url: &str, +) -> Result<(String, u16), ServiceConnectivityError> { + let (raw_body, status) = rest_client.get(url).await?; + #[allow(clippy::manual_range_contains)] + if status < 200 || status >= 300 { let err = format!("GET request {url} failed with status: {status}"); error!("{err}"); return Err(ServiceConnectivityError::new( @@ -78,7 +101,5 @@ where )); } - serde_json::from_str::(&raw_body).map_err(|e| { - ServiceConnectivityError::new(ServiceConnectivityErrorKind::Json, e.to_string()) - }) + Ok((raw_body, status)) } diff --git a/libs/sdk-core/Cargo.toml b/libs/sdk-core/Cargo.toml index 8b2424257..960ac6e6a 100644 --- a/libs/sdk-core/Cargo.toml +++ b/libs/sdk-core/Cargo.toml @@ -54,4 +54,4 @@ regex = { workspace = true } ryu = "1.0.18" [dev-dependencies] -mockito = { workspace = true } +sdk-common = { path = "../sdk-common", features = ["test-utils"] } diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index b66067fbb..33321eecb 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Write; use std::str::FromStr; @@ -13,7 +14,6 @@ use chrono::Local; use futures::TryFutureExt; use gl_client::pb::incoming_payment; use log::{LevelFilter, Metadata, Record}; -use reqwest::{header::CONTENT_TYPE, Body}; use sdk_common::grpc; use sdk_common::prelude::*; use serde::Serialize; @@ -161,6 +161,7 @@ pub struct BreezServices { support_api: Arc, chain_service: Arc, persister: Arc, + rest_client: Arc, payment_receiver: Arc, btc_receive_swapper: Arc, btc_send_swapper: Arc, @@ -427,6 +428,7 @@ impl BreezServices { /// This method will return an [anyhow::Error] when any validation check fails. pub async fn lnurl_pay(&self, req: LnUrlPayRequest) -> Result { match validate_lnurl_pay( + self.rest_client.as_ref(), req.amount_msat, &req.comment, &req.data, @@ -547,7 +549,7 @@ impl BreezServices { .ln_invoice; let lnurl_w_endpoint = req.data.callback.clone(); - let res = validate_lnurl_withdraw(req.data, invoice).await?; + let res = validate_lnurl_withdraw(self.rest_client.as_ref(), req.data, invoice).await?; if let LnUrlWithdrawResult::Ok { ref data } = res { // If endpoint was successfully called, store the LNURL-withdraw endpoint URL as metadata linked to the invoice @@ -578,7 +580,12 @@ impl BreezServices { &self, req_data: LnUrlAuthRequestData, ) -> Result { - Ok(perform_lnurl_auth(&req_data, &SdkLnurlAuthSigner::new(self.node_api.clone())).await?) + Ok(perform_lnurl_auth( + self.rest_client.as_ref(), + &req_data, + &SdkLnurlAuthSigner::new(self.node_api.clone()), + ) + .await?) } /// Creates an bolt11 payment request. @@ -2129,17 +2136,15 @@ impl BreezServices { address: &str, webhook_url: &str, ) -> SdkResult<()> { - get_reqwest_client()? - .post(format!("{}/api/v1/register", self.config.chainnotifier_url)) - .header(CONTENT_TYPE, "application/json") - .body(Body::from( - json!({ - "address": address, - "webhook": webhook_url - }) - .to_string(), - )) - .send() + let url = format!("{}/api/v1/register", self.config.chainnotifier_url); + let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]); + let body = json!({ + "address": address, + "webhook": webhook_url + }) + .to_string(); + self.rest_client + .post(&url, Some(headers), Some(body)) .await .map(|_| ()) .map_err(|e| SdkError::ServiceConnectivity { @@ -2149,19 +2154,14 @@ impl BreezServices { /// Unregisters all onchain tx notifications for the `webhook_url`. async fn unregister_onchain_tx_notifications(&self, webhook_url: &str) -> SdkResult<()> { - get_reqwest_client()? - .post(format!( - "{}/api/v1/unregister", - self.config.chainnotifier_url - )) - .header(CONTENT_TYPE, "application/json") - .body(Body::from( - json!({ - "webhook": webhook_url - }) - .to_string(), - )) - .send() + let url = format!("{}/api/v1/unregister", self.config.chainnotifier_url); + let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]); + let body = json!({ + "webhook": webhook_url + }) + .to_string(); + self.rest_client + .post(&url, Some(headers), Some(body)) .await .map(|_| ()) .map_err(|e| SdkError::ServiceConnectivity { @@ -2239,6 +2239,7 @@ struct BreezServicesBuilder { lsp_api: Option>, fiat_api: Option>, persister: Option>, + rest_client: Option>, support_api: Option>, swapper_api: Option>, /// Reverse swap functionality on the Breez Server @@ -2258,6 +2259,7 @@ impl BreezServicesBuilder { lsp_api: None, fiat_api: None, persister: None, + rest_client: None, support_api: None, swapper_api: None, reverse_swapper_api: None, @@ -2297,6 +2299,11 @@ impl BreezServicesBuilder { self } + pub fn rest_client(&mut self, rest_client: Arc) -> &mut Self { + self.rest_client = Some(rest_client.clone()); + self + } + pub fn swapper_api(&mut self, swapper_api: Arc) -> &mut Self { self.swapper_api = Some(swapper_api.clone()); self @@ -2424,6 +2431,11 @@ impl BreezServicesBuilder { persister: persister.clone(), }); + let rest_client: Arc = match self.rest_client.clone() { + Some(rest_client) => rest_client, + None => Arc::new(ReqwestRestClient::new()?), + }; + // mempool space is used to monitor the chain let mempoolspace_urls = match self.config.mempoolspace_url.clone() { None => { @@ -2444,7 +2456,10 @@ impl BreezServicesBuilder { } Some(mempoolspace_url_from_config) => vec![mempoolspace_url_from_config], }; - let chain_service = Arc::new(RedundantChainService::from_base_urls(mempoolspace_urls)); + let chain_service = Arc::new(RedundantChainService::from_base_urls( + rest_client.clone(), + mempoolspace_urls, + )); let btc_receive_swapper = Arc::new(BTCReceiveSwap::new( self.config.network.into(), @@ -2464,7 +2479,7 @@ impl BreezServicesBuilder { .unwrap_or_else(|| breez_server.clone()), self.reverse_swap_service_api .clone() - .unwrap_or_else(|| Arc::new(BoltzApi {})), + .unwrap_or_else(|| Arc::new(BoltzApi::new(rest_client.clone()))), persister.clone(), chain_service.clone(), unwrapped_node_api.clone(), @@ -2495,6 +2510,7 @@ impl BreezServicesBuilder { buy_bitcoin_api, chain_service, persister: persister.clone(), + rest_client, btc_receive_swapper, btc_send_swapper, payment_receiver, @@ -3299,8 +3315,13 @@ pub(crate) mod tests { #[tokio::test] async fn test_buy_bitcoin_with_moonpay() -> Result<(), Box> { - let breez_services = breez_services().await?; + let mock_rest_client = MockRestClient::new(); + mock_rest_client.add_response(MockResponse::new(200, "800000".to_string())); + let rest_client: Arc = Arc::new(mock_rest_client); + + let breez_services = breez_services_with(None, Some(rest_client.clone()), vec![]).await?; breez_services.sync().await?; + let moonpay_url = breez_services .buy_bitcoin(BuyBitcoinRequest { provider: BuyBitcoinProvider::Moonpay, @@ -3315,7 +3336,9 @@ pub(crate) mod tests { assert_eq!(parsed.host_str(), Some("mock.moonpay")); assert_eq!(parsed.path(), "/"); - let wallet_address = parse(query_pairs.get("wa").unwrap(), None).await?; + let wallet_address = + parse_with_rest_client(rest_client.as_ref(), query_pairs.get("wa").unwrap(), None) + .await?; assert!(matches!(wallet_address, InputType::BitcoinAddress { .. })); let max_amount = query_pairs.get("ma").unwrap(); @@ -3326,16 +3349,19 @@ pub(crate) mod tests { /// Build node service for tests pub(crate) async fn breez_services() -> Result> { - breez_services_with(None, vec![]).await + breez_services_with(None, None, vec![]).await } /// Build node service for tests with a list of known payments pub(crate) async fn breez_services_with( node_api: Option>, + rest_client: Option>, known_payments: Vec, ) -> Result> { let node_api = node_api.unwrap_or_else(|| Arc::new(MockNodeAPI::new(get_dummy_node_state()))); + let rest_client: Arc = + rest_client.unwrap_or_else(|| Arc::new(MockRestClient::new())); let test_config = create_test_config(); let persister = Arc::new(create_test_persister(test_config.clone())); @@ -3351,6 +3377,7 @@ pub(crate) mod tests { .buy_bitcoin_api(Arc::new(MockBuyBitcoinService {})) .persister(persister) .node_api(node_api) + .rest_client(rest_client) .backup_transport(Arc::new(MockBackupTransport::new())) .build(None, None) .await?; diff --git a/libs/sdk-core/src/chain.rs b/libs/sdk-core/src/chain.rs index 350ef435f..ccf93b4d3 100644 --- a/libs/sdk-core/src/chain.rs +++ b/libs/sdk-core/src/chain.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use anyhow::Result; use sdk_common::prelude::*; use serde::{Deserialize, Serialize}; @@ -25,7 +27,7 @@ pub trait ChainService: Send + Sync { } pub trait RedundantChainServiceTrait: ChainService { - fn from_base_urls(base_urls: Vec) -> Self; + fn from_base_urls(rest_client: Arc, base_urls: Vec) -> Self; } #[derive(Clone)] @@ -33,12 +35,12 @@ pub struct RedundantChainService { instances: Vec, } impl RedundantChainServiceTrait for RedundantChainService { - fn from_base_urls(base_urls: Vec) -> Self { + fn from_base_urls(rest_client: Arc, base_urls: Vec) -> Self { Self { instances: base_urls .iter() .map(|url: &String| url.trim_end_matches('/')) - .map(MempoolSpace::from_base_url) + .map(|url| MempoolSpace::from_base_url(rest_client.clone(), url)) .collect(), } } @@ -242,6 +244,7 @@ pub(crate) fn get_total_incoming_txs(address: String, transactions: Vec, pub(crate) base_url: String, } @@ -318,17 +321,18 @@ pub struct Outspend { pub status: Option, } -impl Default for MempoolSpace { - fn default() -> Self { +impl MempoolSpace { + #[allow(dead_code)] + pub fn new(rest_client: Arc) -> MempoolSpace { MempoolSpace { + rest_client, base_url: DEFAULT_MEMPOOL_SPACE_URL.into(), } } -} -impl MempoolSpace { - pub fn from_base_url(base_url: &str) -> MempoolSpace { + pub fn from_base_url(rest_client: Arc, base_url: &str) -> MempoolSpace { MempoolSpace { + rest_client, base_url: base_url.into(), } } @@ -337,33 +341,50 @@ impl MempoolSpace { #[tonic::async_trait] impl ChainService for MempoolSpace { async fn recommended_fees(&self) -> SdkResult { - get_parse_and_log_response(&format!("{}/v1/fees/recommended", self.base_url), true) - .await - .map_err(Into::into) + let (response, _) = get_and_check_success( + self.rest_client.as_ref(), + &format!("{}/v1/fees/recommended", self.base_url), + ) + .await?; + Ok(parse_json(&response)?) } async fn address_transactions(&self, address: String) -> SdkResult> { - get_parse_and_log_response(&format!("{}/address/{address}/txs", self.base_url), true) - .await - .map_err(Into::into) + let (response, _) = get_and_check_success( + self.rest_client.as_ref(), + &format!("{}/address/{address}/txs", self.base_url), + ) + .await?; + Ok(parse_json(&response)?) } async fn current_tip(&self) -> SdkResult { - get_parse_and_log_response(&format!("{}/blocks/tip/height", self.base_url), true) - .await - .map_err(Into::into) + let (response, _) = get_and_check_success( + self.rest_client.as_ref(), + &format!("{}/blocks/tip/height", self.base_url), + ) + .await?; + Ok(parse_json(&response)?) } async fn transaction_outspends(&self, txid: String) -> SdkResult> { - let url = format!("{}/tx/{txid}/outspends", self.base_url); - get_parse_and_log_response(&url, true) - .await - .map_err(Into::into) + let (response, _) = get_and_check_success( + self.rest_client.as_ref(), + &format!("{}/tx/{txid}/outspends", self.base_url), + ) + .await?; + Ok(parse_json(&response)?) } async fn broadcast_transaction(&self, tx: Vec) -> SdkResult { - let txid_or_error = - post_and_log_response(&format!("{}/tx", self.base_url), Some(hex::encode(tx))).await?; + let (txid_or_error, _) = self + .rest_client + .post( + &format!("{}/tx", self.base_url), + None, + Some(hex::encode(tx)), + ) + .await?; match txid_or_error.contains("error") { true => Err(SdkError::Generic { err: format!("Error fetching tx: {txid_or_error}"), @@ -374,52 +395,120 @@ impl ChainService for MempoolSpace { } #[cfg(test)] mod tests { + use std::sync::Arc; + use crate::{ chain::{MempoolSpace, OnchainTx, RedundantChainService, RedundantChainServiceTrait}, error::SdkError, }; use anyhow::Result; + use sdk_common::prelude::{MockResponse, MockRestClient, RestClient}; + use serde_json::json; use tokio::test; use super::ChainService; #[test] async fn test_recommended_fees() -> Result<()> { - let ms = MempoolSpace::default(); + let mock_rest_client = MockRestClient::new(); + + let response_body = json!({ + "economyFee": 2, + "fastestFee": 3, + "halfHourFee": 2, + "hourFee": 2, + "minimumFee": 1, + }) + .to_string(); + + mock_rest_client.add_response(MockResponse::new(200, response_body)); + let rest_client: Arc = Arc::new(mock_rest_client); + + let ms = MempoolSpace::new(rest_client); let fees = ms.recommended_fees().await?; - assert!(fees.economy_fee > 0); - assert!(fees.fastest_fee > 0); - assert!(fees.half_hour_fee > 0); - assert!(fees.hour_fee > 0); - assert!(fees.minimum_fee > 0); + assert_eq!(fees.economy_fee, 2); + assert_eq!(fees.fastest_fee, 3); + assert_eq!(fees.half_hour_fee, 2); + assert_eq!(fees.hour_fee, 2); + assert_eq!(fees.minimum_fee, 1); Ok(()) } #[test] async fn test_recommended_fees_with_fallback() -> Result<()> { - let ms = RedundantChainService::from_base_urls(vec![ - "https://mempool-url-unreachable.space/api/".into(), - ]); + let mock_rest_client = MockRestClient::new(); + + let unreachable_response_body = ""; + let response_body = json!({ + "economyFee": 2, + "fastestFee": 3, + "halfHourFee": 2, + "hourFee": 2, + "minimumFee": 1, + }); + + mock_rest_client.add_response(MockResponse::new( + 400, + unreachable_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new( + 400, + unreachable_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new(200, response_body.to_string())); + mock_rest_client.add_response(MockResponse::new( + 400, + unreachable_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new( + 400, + unreachable_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new( + 400, + unreachable_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new( + 400, + unreachable_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new(200, response_body.to_string())); + + let rest_client: Arc = Arc::new(mock_rest_client); + + let ms = RedundantChainService::from_base_urls( + rest_client.clone(), + vec!["https://mempool-url-unreachable.space/api/".into()], + ); assert!(ms.recommended_fees().await.is_err()); - let ms = RedundantChainService::from_base_urls(vec![ - "https://mempool-url-unreachable.space/api/".into(), - "https://mempool.emzy.de/api/".into(), - ]); + let ms = RedundantChainService::from_base_urls( + rest_client.clone(), + vec![ + "https://mempool-url-unreachable.space/api/".into(), + "https://mempool.emzy.de/api/".into(), + ], + ); assert!(ms.recommended_fees().await.is_ok()); - let ms = RedundantChainService::from_base_urls(vec![ - "https://mempool-url-unreachable.space/api/".into(), - "https://another-mempool-url-unreachable.space/api/".into(), - ]); + let ms = RedundantChainService::from_base_urls( + rest_client.clone(), + vec![ + "https://mempool-url-unreachable.space/api/".into(), + "https://another-mempool-url-unreachable.space/api/".into(), + ], + ); assert!(ms.recommended_fees().await.is_err()); - let ms = RedundantChainService::from_base_urls(vec![ - "https://mempool-url-unreachable.space/api/".into(), - "https://another-mempool-url-unreachable.space/api/".into(), - "https://mempool.emzy.de/api/".into(), - ]); + let ms = RedundantChainService::from_base_urls( + rest_client, + vec![ + "https://mempool-url-unreachable.space/api/".into(), + "https://another-mempool-url-unreachable.space/api/".into(), + "https://mempool.emzy.de/api/".into(), + ], + ); assert!(ms.recommended_fees().await.is_ok()); Ok(()) @@ -427,15 +516,30 @@ mod tests { #[test] async fn test_address_transactions() -> Result<()> { - let ms = MempoolSpace::default(); - let mut txs = ms + let mock_rest_client = MockRestClient::new(); + + let address_transactions_response_body = r#"[{"txid":"5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985","version":2,"locktime":0,"vin":[{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","vout":0,"prevout":{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},"scriptsig":"","scriptsig_asm":"","witness":["3045022100a2f0ac810ce88625890f7e212d175eb1cd6b7c73ffed95a2bec06b38e0b2de060220036675c6a5c89845988cc27e7acba772e7655f2abb0575449471d8323d5900b301","026b815dddaf1687a05349d75d25911c9b6e2381e55ba72148009cfa0a577c89d9"],"is_coinbase":false,"sequence":0},{"txid":"6d6766c283093e2d043ae877bb915175b3d8672a20f0459300267aaab1b5766a","vout":0,"prevout":{"scriptpubkey":"001485b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 85b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qskencxfhqk8dpz6mzghrpjh33enuev5zh0mrjw","value":33247},"scriptsig":"","scriptsig_asm":"","witness":["304402200272cac1a312aae2a4ee64150e5b26e611a56509a467176e38c905b632d3ce56022005497d0d3ff14911214cb0fbb22a1aa16830ba669f6ff38723684750ceb4b11a01","0397d3b72557bd2044508ee3b22d1216b3f871c0963500f8c8dc6a143ee7a6a206"],"is_coinbase":false,"sequence":0},{"txid":"81af33ae00a9dadeb83b915b05742e986a470fff7456540e3f018deb94abda0e","vout":1,"prevout":{"scriptpubkey":"001431505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 31505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qx9g9v3cfydr6hv8y62357emnka9fn8294e73yl","value":172952},"scriptsig":"","scriptsig_asm":"","witness":["30450221008426c1b3d535f10c7cbccec6be3ea9be3514f3a86bf234584722665325283f35022010b6a617a465d1d7eea45562632f0ab80b0894da44b67fab65191a98fd9d3acb01","0221250914423379d3caf662297e8069621ca2c362cf92107388483929f4d9eb67"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001459c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 59c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt8rscz0j9vdmqp6rnt6rk6qf663tcvd44f6gxa","value":2920},{"scriptpubkey":"00202c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 2c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1q93qyum5uf5pjyeaznfs8f3wmjveudn9wpjw5xr8dve33vgea3shs9jhvww","value":442557}],"size":532,"weight":1153,"fee":23938,"status":{"confirmed":true,"block_height":674358,"block_hash":"00000000000000000004c6171622f56692cc480d3c76ecae4355e69699a6ae44","block_time":1615595727}},{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","version":2,"locktime":0,"vin":[{"txid":"9332d8d11d81c3b674caff75db5543491e7f22e619ecc034bedf4a007518fe3a","vout":0,"prevout":{"scriptpubkey":"001415f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 15f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qzhcd446gq6crvyngwqudfad6kgq2lnuw9r2a86","value":470675},"scriptsig":"","scriptsig_asm":"","witness":["3045022100f30d84532f96b5e489047174e81394883cd519d427ca8f4facc2366f718cc678022007c083634402f40708c645cd0c1a2757b56de2076ca6ee856e514859381cd93801","02942b44eb4289e3af0aeeb73dfa82b0a5c8a3a06ae85bfd22aa3dcfcd64096462"],"is_coinbase":false,"sequence":0},{"txid":"c62da0c2d1929ab2a2c04d4fbae2a6e4e947f867cba584d1f80c4a1a62f4a75f","vout":1,"prevout":{"scriptpubkey":"0014f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7rqaddr36hj2fqluz3k5yg9yaq2c00c3tw4qy5","value":899778},"scriptsig":"","scriptsig_asm":"","witness":["304402202da0eac25786003181526c4fe1592f982aa8d0f32c642a5103cdebbf4aa8b5a80220750cd6859bfb9a7df8d7c4d79a70e17a6df87f150fe1fdaade4650332ef0f47c01","02ecab80fcfe949633064c25fc33854fd09b8730decdf679db1f429bce201ec685"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},{"scriptpubkey":"00200cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 0cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1qpn4xpt57afp7vjchhfj6fstm6wk0nkkrq7p9mmdgt4dqjvvpm0qqlxqrns","value":1088924}],"size":383,"weight":881,"fee":18313,"status":{"confirmed":true,"block_height":674357,"block_hash":"00000000000000000008d0d007995a8bc9d60de17bd6b55e28a6e4c6918cb206","block_time":1615594996}}]"#; + let transaction_outspends_response_body = r#"[{"spent":true,"txid":"4da22eff957b855c8bde2d8b61bdb9e10add799a04c709dd7142cc796cee0b65","vin":1,"status":{"confirmed":true,"block_height":674365,"block_hash":"000000000000000000038f780364221846a3c11e2a5b33eee69029afe5775a0f","block_time":1615598852}},{"spent":true,"txid":"61585c400d8cfe490d3d3c6e1e3177edb9b6f43e337772530ab32ea4e54db3b4","vin":0,"status":{"confirmed":true,"block_height":797168,"block_hash":"0000000000000000000569b9dca483f10ed6c2bf9245b5a9b45519dd4f3dd40d","block_time":1688489603}}]"#; + mock_rest_client.add_response(MockResponse::new( + 200, + address_transactions_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new( + 200, + transaction_outspends_response_body.to_string(), + )); + mock_rest_client.add_response(MockResponse::new(404, "".to_string())); + + let rest_client: Arc = Arc::new(mock_rest_client); + + let ms = MempoolSpace::new(rest_client); + let txs = ms .address_transactions("bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2".to_string()) .await?; - txs.sort_unstable_by(|tx_a, tx_b| tx_b.txid.cmp(&tx_a.txid)); let serialized_res = serde_json::to_string(&txs)?; - let expected = r#"[{"txid":"5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985","version":2,"locktime":0,"vin":[{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","vout":0,"prevout":{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},"scriptsig":"","scriptsig_asm":"","witness":["3045022100a2f0ac810ce88625890f7e212d175eb1cd6b7c73ffed95a2bec06b38e0b2de060220036675c6a5c89845988cc27e7acba772e7655f2abb0575449471d8323d5900b301","026b815dddaf1687a05349d75d25911c9b6e2381e55ba72148009cfa0a577c89d9"],"is_coinbase":false,"sequence":0},{"txid":"6d6766c283093e2d043ae877bb915175b3d8672a20f0459300267aaab1b5766a","vout":0,"prevout":{"scriptpubkey":"001485b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 85b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qskencxfhqk8dpz6mzghrpjh33enuev5zh0mrjw","value":33247},"scriptsig":"","scriptsig_asm":"","witness":["304402200272cac1a312aae2a4ee64150e5b26e611a56509a467176e38c905b632d3ce56022005497d0d3ff14911214cb0fbb22a1aa16830ba669f6ff38723684750ceb4b11a01","0397d3b72557bd2044508ee3b22d1216b3f871c0963500f8c8dc6a143ee7a6a206"],"is_coinbase":false,"sequence":0},{"txid":"81af33ae00a9dadeb83b915b05742e986a470fff7456540e3f018deb94abda0e","vout":1,"prevout":{"scriptpubkey":"001431505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 31505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qx9g9v3cfydr6hv8y62357emnka9fn8294e73yl","value":172952},"scriptsig":"","scriptsig_asm":"","witness":["30450221008426c1b3d535f10c7cbccec6be3ea9be3514f3a86bf234584722665325283f35022010b6a617a465d1d7eea45562632f0ab80b0894da44b67fab65191a98fd9d3acb01","0221250914423379d3caf662297e8069621ca2c362cf92107388483929f4d9eb67"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001459c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 59c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt8rscz0j9vdmqp6rnt6rk6qf663tcvd44f6gxa","value":2920},{"scriptpubkey":"00202c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 2c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1q93qyum5uf5pjyeaznfs8f3wmjveudn9wpjw5xr8dve33vgea3shs9jhvww","value":442557}],"size":532,"weight":1153,"fee":23938,"status":{"confirmed":true,"block_height":674358,"block_hash":"00000000000000000004c6171622f56692cc480d3c76ecae4355e69699a6ae44","block_time":1615595727}},{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","version":2,"locktime":0,"vin":[{"txid":"9332d8d11d81c3b674caff75db5543491e7f22e619ecc034bedf4a007518fe3a","vout":0,"prevout":{"scriptpubkey":"001415f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 15f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qzhcd446gq6crvyngwqudfad6kgq2lnuw9r2a86","value":470675},"scriptsig":"","scriptsig_asm":"","witness":["3045022100f30d84532f96b5e489047174e81394883cd519d427ca8f4facc2366f718cc678022007c083634402f40708c645cd0c1a2757b56de2076ca6ee856e514859381cd93801","02942b44eb4289e3af0aeeb73dfa82b0a5c8a3a06ae85bfd22aa3dcfcd64096462"],"is_coinbase":false,"sequence":0},{"txid":"c62da0c2d1929ab2a2c04d4fbae2a6e4e947f867cba584d1f80c4a1a62f4a75f","vout":1,"prevout":{"scriptpubkey":"0014f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7rqaddr36hj2fqluz3k5yg9yaq2c00c3tw4qy5","value":899778},"scriptsig":"","scriptsig_asm":"","witness":["304402202da0eac25786003181526c4fe1592f982aa8d0f32c642a5103cdebbf4aa8b5a80220750cd6859bfb9a7df8d7c4d79a70e17a6df87f150fe1fdaade4650332ef0f47c01","02ecab80fcfe949633064c25fc33854fd09b8730decdf679db1f429bce201ec685"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},{"scriptpubkey":"00200cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 0cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1qpn4xpt57afp7vjchhfj6fstm6wk0nkkrq7p9mmdgt4dqjvvpm0qqlxqrns","value":1088924}],"size":383,"weight":881,"fee":18313,"status":{"confirmed":true,"block_height":674357,"block_hash":"00000000000000000008d0d007995a8bc9d60de17bd6b55e28a6e4c6918cb206","block_time":1615594996}}]"#; - let expected_txs: Vec = serde_json::from_str(expected)?; + let expected_txs: Vec = + serde_json::from_str(address_transactions_response_body)?; let expected_serialized = serde_json::to_string(&expected_txs)?; assert_eq!(expected_serialized, serialized_res); @@ -456,7 +560,7 @@ mod tests { Ok(_) => panic!("Expected an error"), Err(e) => match e { SdkError::ServiceConnectivity { err } => { - assert_eq!(err, "GET request https://mempool.space/api/tx/07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf6901/outspends failed with status: 404 Not Found") + assert_eq!(err, "GET request https://mempool.space/api/tx/07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf6901/outspends failed with status: 404") } _ => panic!("Expected a service connectivity error"), }, @@ -467,15 +571,19 @@ mod tests { // #[test] // async fn test_address_transactions_mempool() { - // let ms = MempoolSpace::default(); + // let mock_rest_client = MockRestClient::new(); + // let ms = MempoolSpace::new(mock_rest_client); + // + // let response_body = r#"[{"txid":"5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985","version":2,"locktime":0,"vin":[{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","vout":0,"prevout":{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},"scriptsig":"","scriptsig_asm":"","witness":["3045022100a2f0ac810ce88625890f7e212d175eb1cd6b7c73ffed95a2bec06b38e0b2de060220036675c6a5c89845988cc27e7acba772e7655f2abb0575449471d8323d5900b301","026b815dddaf1687a05349d75d25911c9b6e2381e55ba72148009cfa0a577c89d9"],"is_coinbase":false,"sequence":0},{"txid":"6d6766c283093e2d043ae877bb915175b3d8672a20f0459300267aaab1b5766a","vout":0,"prevout":{"scriptpubkey":"001485b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 85b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qskencxfhqk8dpz6mzghrpjh33enuev5zh0mrjw","value":33247},"scriptsig":"","scriptsig_asm":"","witness":["304402200272cac1a312aae2a4ee64150e5b26e611a56509a467176e38c905b632d3ce56022005497d0d3ff14911214cb0fbb22a1aa16830ba669f6ff38723684750ceb4b11a01","0397d3b72557bd2044508ee3b22d1216b3f871c0963500f8c8dc6a143ee7a6a206"],"is_coinbase":false,"sequence":0},{"txid":"81af33ae00a9dadeb83b915b05742e986a470fff7456540e3f018deb94abda0e","vout":1,"prevout":{"scriptpubkey":"001431505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 31505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qx9g9v3cfydr6hv8y62357emnka9fn8294e73yl","value":172952},"scriptsig":"","scriptsig_asm":"","witness":["30450221008426c1b3d535f10c7cbccec6be3ea9be3514f3a86bf234584722665325283f35022010b6a617a465d1d7eea45562632f0ab80b0894da44b67fab65191a98fd9d3acb01","0221250914423379d3caf662297e8069621ca2c362cf92107388483929f4d9eb67"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001459c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 59c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt8rscz0j9vdmqp6rnt6rk6qf663tcvd44f6gxa","value":2920},{"scriptpubkey":"00202c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 2c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1q93qyum5uf5pjyeaznfs8f3wmjveudn9wpjw5xr8dve33vgea3shs9jhvww","value":442557}],"size":532,"weight":1153,"fee":23938,"status":{"confirmed":true,"block_height":674358,"block_hash":"00000000000000000004c6171622f56692cc480d3c76ecae4355e69699a6ae44","block_time":1615595727}},{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","version":2,"locktime":0,"vin":[{"txid":"9332d8d11d81c3b674caff75db5543491e7f22e619ecc034bedf4a007518fe3a","vout":0,"prevout":{"scriptpubkey":"001415f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 15f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qzhcd446gq6crvyngwqudfad6kgq2lnuw9r2a86","value":470675},"scriptsig":"","scriptsig_asm":"","witness":["3045022100f30d84532f96b5e489047174e81394883cd519d427ca8f4facc2366f718cc678022007c083634402f40708c645cd0c1a2757b56de2076ca6ee856e514859381cd93801","02942b44eb4289e3af0aeeb73dfa82b0a5c8a3a06ae85bfd22aa3dcfcd64096462"],"is_coinbase":false,"sequence":0},{"txid":"c62da0c2d1929ab2a2c04d4fbae2a6e4e947f867cba584d1f80c4a1a62f4a75f","vout":1,"prevout":{"scriptpubkey":"0014f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7rqaddr36hj2fqluz3k5yg9yaq2c00c3tw4qy5","value":899778},"scriptsig":"","scriptsig_asm":"","witness":["304402202da0eac25786003181526c4fe1592f982aa8d0f32c642a5103cdebbf4aa8b5a80220750cd6859bfb9a7df8d7c4d79a70e17a6df87f150fe1fdaade4650332ef0f47c01","02ecab80fcfe949633064c25fc33854fd09b8730decdf679db1f429bce201ec685"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},{"scriptpubkey":"00200cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 0cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1qpn4xpt57afp7vjchhfj6fstm6wk0nkkrq7p9mmdgt4dqjvvpm0qqlxqrns","value":1088924}],"size":383,"weight":881,"fee":18313,"status":{"confirmed":true,"block_height":674357,"block_hash":"00000000000000000008d0d007995a8bc9d60de17bd6b55e28a6e4c6918cb206","block_time":1615594996}}]"#; + // mock_rest_client.add_response(MockResponse::new(200, response_body.to_string())); + // // let txs = ms // .address_transactions("1N4f3y3LYJZ2Qd9FyPt3AcHp451qt12paR".to_string()) // .await // .unwrap(); // let serialized_res = serde_json::to_string(&txs).unwrap(); - // let expected = r#"[{"txid":"5e0668bf1cd24f2f8656ee82d4886f5303a06b26838e24b7db73afc59e228985","version":2,"locktime":0,"vin":[{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","vout":0,"prevout":{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},"scriptsig":"","scriptsig_asm":"","witness":["3045022100a2f0ac810ce88625890f7e212d175eb1cd6b7c73ffed95a2bec06b38e0b2de060220036675c6a5c89845988cc27e7acba772e7655f2abb0575449471d8323d5900b301","026b815dddaf1687a05349d75d25911c9b6e2381e55ba72148009cfa0a577c89d9"],"is_coinbase":false,"sequence":0},{"txid":"6d6766c283093e2d043ae877bb915175b3d8672a20f0459300267aaab1b5766a","vout":0,"prevout":{"scriptpubkey":"001485b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 85b33c1937058ed08b5b122e30caf18e67ccb282","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qskencxfhqk8dpz6mzghrpjh33enuev5zh0mrjw","value":33247},"scriptsig":"","scriptsig_asm":"","witness":["304402200272cac1a312aae2a4ee64150e5b26e611a56509a467176e38c905b632d3ce56022005497d0d3ff14911214cb0fbb22a1aa16830ba669f6ff38723684750ceb4b11a01","0397d3b72557bd2044508ee3b22d1216b3f871c0963500f8c8dc6a143ee7a6a206"],"is_coinbase":false,"sequence":0},{"txid":"81af33ae00a9dadeb83b915b05742e986a470fff7456540e3f018deb94abda0e","vout":1,"prevout":{"scriptpubkey":"001431505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 31505647092347abb0e4d2a34f6773b74a999d45","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qx9g9v3cfydr6hv8y62357emnka9fn8294e73yl","value":172952},"scriptsig":"","scriptsig_asm":"","witness":["30450221008426c1b3d535f10c7cbccec6be3ea9be3514f3a86bf234584722665325283f35022010b6a617a465d1d7eea45562632f0ab80b0894da44b67fab65191a98fd9d3acb01","0221250914423379d3caf662297e8069621ca2c362cf92107388483929f4d9eb67"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001459c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 59c70c09f22b1bb007439af43b6809d6a2bc31b5","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt8rscz0j9vdmqp6rnt6rk6qf663tcvd44f6gxa","value":2920},{"scriptpubkey":"00202c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 2c404e6e9c4d032267a29a6074c5db9333c6ccae0c9d430ced666316233d8c2f","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1q93qyum5uf5pjyeaznfs8f3wmjveudn9wpjw5xr8dve33vgea3shs9jhvww","value":442557}],"size":532,"weight":1153,"fee":23938,"status":{"confirmed":true,"block_height":674358,"block_hash":"00000000000000000004c6171622f56692cc480d3c76ecae4355e69699a6ae44","block_time":1615595727}},{"txid":"07c9d3fbffc20f96ea7c93ef3bcdf346c8a8456c25850ea76be62b24a7cf690c","version":2,"locktime":0,"vin":[{"txid":"9332d8d11d81c3b674caff75db5543491e7f22e619ecc034bedf4a007518fe3a","vout":0,"prevout":{"scriptpubkey":"001415f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 15f0dad74806b03612687038d4f5bab200afcf8e","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qzhcd446gq6crvyngwqudfad6kgq2lnuw9r2a86","value":470675},"scriptsig":"","scriptsig_asm":"","witness":["3045022100f30d84532f96b5e489047174e81394883cd519d427ca8f4facc2366f718cc678022007c083634402f40708c645cd0c1a2757b56de2076ca6ee856e514859381cd93801","02942b44eb4289e3af0aeeb73dfa82b0a5c8a3a06ae85bfd22aa3dcfcd64096462"],"is_coinbase":false,"sequence":0},{"txid":"c62da0c2d1929ab2a2c04d4fbae2a6e4e947f867cba584d1f80c4a1a62f4a75f","vout":1,"prevout":{"scriptpubkey":"0014f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0c1d6b471d5e4a483fc146d4220a4e81587bf11","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7rqaddr36hj2fqluz3k5yg9yaq2c00c3tw4qy5","value":899778},"scriptsig":"","scriptsig_asm":"","witness":["304402202da0eac25786003181526c4fe1592f982aa8d0f32c642a5103cdebbf4aa8b5a80220750cd6859bfb9a7df8d7c4d79a70e17a6df87f150fe1fdaade4650332ef0f47c01","02ecab80fcfe949633064c25fc33854fd09b8730decdf679db1f429bce201ec685"],"is_coinbase":false,"sequence":0}],"vout":[{"scriptpubkey":"001465c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 65c96c830168b8f0b584294d3b9716bb8584c2d8","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qvhykeqcpdzu0pdvy99xnh9ckhwzcfskct6h6l2","value":263216},{"scriptpubkey":"00200cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_32 0cea60ae9eea43e64b17ba65a4c17bd3acf9dac307825deda85d5a093181dbc0","scriptpubkey_type":"v0_p2wsh","scriptpubkey_address":"bc1qpn4xpt57afp7vjchhfj6fstm6wk0nkkrq7p9mmdgt4dqjvvpm0qqlxqrns","value":1088924}],"size":383,"weight":881,"fee":18313,"status":{"confirmed":true,"block_height":674357,"block_hash":"00000000000000000008d0d007995a8bc9d60de17bd6b55e28a6e4c6918cb206","block_time":1615594996}}]"#; - // let expected_txs: Vec = serde_json::from_str(expected).unwrap(); + // let expected_txs: Vec = serde_json::from_str(response_body).unwrap(); // let expected_serialized = serde_json::to_string(&expected_txs).unwrap(); // assert_eq!(expected_serialized, serialized_res); diff --git a/libs/sdk-core/src/error.rs b/libs/sdk-core/src/error.rs index 6da9b53a3..aba23da92 100644 --- a/libs/sdk-core/src/error.rs +++ b/libs/sdk-core/src/error.rs @@ -57,6 +57,12 @@ impl From for ConnectError { } } +impl From for ConnectError { + fn from(value: ServiceConnectivityError) -> Self { + Self::ServiceConnectivity { err: value.err } + } +} + impl From for ConnectError { fn from(value: SdkError) -> Self { match value { diff --git a/libs/sdk-core/src/lnurl/mod.rs b/libs/sdk-core/src/lnurl/mod.rs index b4f5b9bdd..e80638c00 100644 --- a/libs/sdk-core/src/lnurl/mod.rs +++ b/libs/sdk-core/src/lnurl/mod.rs @@ -1,20 +1,2 @@ pub mod auth; pub mod pay; - -#[cfg(test)] -mod tests { - use std::sync::Mutex; - - use mockito::Server; - use once_cell::sync::Lazy; - - pub(crate) static MOCK_HTTP_SERVER: Lazy> = Lazy::new(|| { - let opts = mockito::ServerOpts { - host: "127.0.0.1", - port: 8080, - ..Default::default() - }; - let server = Server::new_with_opts(opts); - Mutex::new(server) - }); -} diff --git a/libs/sdk-core/src/lnurl/pay.rs b/libs/sdk-core/src/lnurl/pay.rs index 727bb3133..daa415120 100644 --- a/libs/sdk-core/src/lnurl/pay.rs +++ b/libs/sdk-core/src/lnurl/pay.rs @@ -35,269 +35,170 @@ pub(crate) mod tests { use anyhow::{anyhow, Result}; use gl_client::bitcoin::hashes::hex::ToHex; use gl_client::pb::cln::pay_response::PayStatus; - use mockito::Mock; use rand::random; + use serde_json::json; use crate::bitcoin::hashes::{sha256, Hash}; - use crate::breez_services::tests::get_dummy_node_state; + use crate::breez_services::tests::{breez_services_with, get_dummy_node_state}; use crate::lnurl::pay::*; - use crate::lnurl::tests::MOCK_HTTP_SERVER; use crate::{test_utils::*, LnUrlPayRequest}; - struct LnurlPayCallbackParams<'a> { - pay_req: &'a LnUrlPayRequestData, - user_amount_msat: u64, + struct LnurlPayCallbackParams { error: Option, pr: Option, - comment: String, } - struct AesPayCallbackParams<'a> { - pay_req: &'a LnUrlPayRequestData, - user_amount_msat: u64, + struct AesPayCallbackParams { error: Option, pr: Option, sa_data: AesSuccessActionDataDecrypted, iv_bytes: [u8; 16], key_bytes: [u8; 32], - comment: String, } /// Mock an LNURL-pay endpoint that responds with no Success Action fn mock_lnurl_pay_callback_endpoint_no_success_action( + mock_rest_client: &MockRestClient, callback_params: LnurlPayCallbackParams, - ) -> Result { - let LnurlPayCallbackParams { - pay_req, - user_amount_msat, - error, - pr, - comment, - } = callback_params; - - let callback_url = build_pay_callback_url(user_amount_msat, &Some(comment), pay_req)?; - let url = reqwest::Url::parse(&callback_url)?; - let mockito_path: &str = &format!("{}?{}", url.path(), url.query().unwrap()); - - let expected_payload = r#" -{ - "pr":"token-invoice", - "routes":[] -} - "# - .replace('\n', "") - .replace( - "token-invoice", - &pr.unwrap_or_else(|| "token-invoice".to_string()), - ); + ) { + let LnurlPayCallbackParams { error, pr } = callback_params; let response_body = match error { - None => expected_payload, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + None => json!({ + "pr": pr.unwrap_or_else(|| "token-invoice".to_string()), + "routes": [] + }) + .to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - let mut server = MOCK_HTTP_SERVER.lock().unwrap(); - Ok(server - .mock("GET", mockito_path) - .with_body(response_body) - .create()) + mock_rest_client.add_response(MockResponse::new(200, response_body)); } /// Mock an LNURL-pay endpoint that responds with an unsupported Success Action fn mock_lnurl_pay_callback_endpoint_unsupported_success_action( + mock_rest_client: &MockRestClient, callback_params: LnurlPayCallbackParams, - ) -> Result { - let LnurlPayCallbackParams { - pay_req, - user_amount_msat, - error, - pr: _pr, - comment, - } = callback_params; - - let callback_url = build_pay_callback_url(user_amount_msat, &Some(comment), pay_req)?; - let url = reqwest::Url::parse(&callback_url)?; - let mockito_path: &str = &format!("{}?{}", url.path(), url.query().unwrap()); - - let expected_payload = r#" -{ - "pr":"lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz", - "routes":[], - "successAction": { - "tag":"random-type-that-is-not-supported", - "message":"test msg" - } -} - "#.replace('\n', ""); + ) { + let LnurlPayCallbackParams { error, pr } = callback_params; let response_body = match error { - None => expected_payload, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + None => json!({ + "pr": pr.unwrap_or_else(|| "token-invoice".to_string()), + "routes": [], + "successAction": { + "tag": "random-type-that-is-not-supported", + "message": "test msg" + } + }) + .to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - let mut server = MOCK_HTTP_SERVER.lock().unwrap(); - Ok(server - .mock("GET", mockito_path) - .with_body(response_body) - .create()) + mock_rest_client.add_response(MockResponse::new(200, response_body)); } /// Mock an LNURL-pay endpoint that responds with a Success Action of type message fn mock_lnurl_pay_callback_endpoint_msg_success_action( + mock_rest_client: &MockRestClient, callback_params: LnurlPayCallbackParams, - ) -> Result { - let LnurlPayCallbackParams { - pay_req, - user_amount_msat, - error, - pr, - comment, - } = callback_params; - - let callback_url = build_pay_callback_url(user_amount_msat, &Some(comment), pay_req)?; - let url = reqwest::Url::parse(&callback_url)?; - let mockito_path: &str = &format!("{}?{}", url.path(), url.query().unwrap()); - - let expected_payload = r#" -{ - "pr":"token-invoice", - "routes":[], - "successAction": { - "tag":"message", - "message":"test msg" - } -} - "# - .replace('\n', "") - .replace( - "token-invoice", - &pr.unwrap_or_else(|| "token-invoice".to_string()), - ); + ) { + let LnurlPayCallbackParams { error, pr } = callback_params; let response_body = match error { - None => expected_payload, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + None => json!({ + "pr": pr.unwrap_or_else(|| "token-invoice".to_string()), + "routes":[], + "successAction": { + "tag": "message", + "message": "test msg" + } + }) + .to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - let mut server = MOCK_HTTP_SERVER.lock().unwrap(); - Ok(server - .mock("GET", mockito_path) - .with_body(response_body) - .create()) + mock_rest_client.add_response(MockResponse::new(200, response_body)); } /// Mock an LNURL-pay endpoint that responds with a Success Action of type URL fn mock_lnurl_pay_callback_endpoint_url_success_action( + mock_rest_client: &MockRestClient, callback_params: LnurlPayCallbackParams, success_action_url: Option<&str>, - ) -> Result { - let LnurlPayCallbackParams { - pay_req, - user_amount_msat, - error, - pr, - comment, - } = callback_params; - - let callback_url = build_pay_callback_url(user_amount_msat, &Some(comment), pay_req)?; - let url = reqwest::Url::parse(&callback_url)?; - let mockito_path: &str = &format!("{}?{}", url.path(), url.query().unwrap()); - - let expected_payload = r#" -{ - "pr":"token-invoice", - "routes":[], - "successAction": { - "tag":"url", - "description":"test description", - "url":"success-action-url" - } -} - "# - .replace('\n', "") - .replace( - "token-invoice", - &pr.unwrap_or_else(|| "token-invoice".to_string()), - ) - .replace( - "success-action-url", - success_action_url.unwrap_or("http://localhost:8080/test-url"), - ); + ) { + let LnurlPayCallbackParams { error, pr } = callback_params; let response_body = match error { - None => expected_payload, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + None => json!({ + "pr": pr.unwrap_or_else(|| "token-invoice".to_string()), + "routes":[], + "successAction": { + "tag": "url", + "description": "test description", + "url": success_action_url.unwrap_or("http://localhost:8080/test-url"), + } + }) + .to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - let mut server = MOCK_HTTP_SERVER.lock().unwrap(); - Ok(server - .mock("GET", mockito_path) - .with_body(response_body) - .create()) + mock_rest_client.add_response(MockResponse::new(200, response_body)); } /// Mock an LNURL-pay endpoint that responds with a Success Action of type AES fn mock_lnurl_pay_callback_endpoint_aes_success_action( + mock_rest_client: &MockRestClient, aes_callback_params: AesPayCallbackParams, - ) -> Result { + ) { let AesPayCallbackParams { - pay_req, - user_amount_msat, error, pr, sa_data, iv_bytes, key_bytes, - comment, } = aes_callback_params; - let callback_url = build_pay_callback_url(user_amount_msat, &Some(comment), pay_req)?; - let url = reqwest::Url::parse(&callback_url)?; - let mockito_path: &str = &format!("{}?{}", url.path(), url.query().unwrap()); let iv_base64 = base64::encode(iv_bytes); - let cipertext = AesSuccessActionData::encrypt(&key_bytes, &iv_bytes, sa_data.plaintext)?; - - let expected_payload = r#" -{ - "pr":"token-invoice", - "routes":[], - "successAction": { - "tag":"aes", - "description":"token-description", - "iv":"token-iv", - "ciphertext":"token-ciphertext" - } -} - "# - .replace('\n', "") - .replace("token-iv", &iv_base64) - .replace("token-ciphertext", &cipertext) - .replace("token-description", &sa_data.description) - .replace( - "token-invoice", - &pr.unwrap_or_else(|| "token-invoice".to_string()), - ); + let cipertext = + AesSuccessActionData::encrypt(&key_bytes, &iv_bytes, sa_data.plaintext).unwrap(); let response_body = match error { - None => expected_payload, - Some(err_reason) => { - ["{\"status\": \"ERROR\", \"reason\": \"", &err_reason, "\"}"].join("") - } + None => json!({ + "pr": pr.unwrap_or_else(|| "token-invoice".to_string()), + "routes": [], + "successAction": { + "tag": "aes", + "description": sa_data.description, + "iv": iv_base64, + "ciphertext": cipertext + } + }) + .to_string(), + Some(err_reason) => json!({ + "status": "ERROR", + "reason": err_reason + }) + .to_string(), }; - let mut server = MOCK_HTTP_SERVER.lock().unwrap(); - Ok(server - .mock("GET", mockito_path) - .with_body(response_body) - .create()) + mock_rest_client.add_response(MockResponse::new(200, response_body)); } fn get_test_pay_req_data( @@ -383,21 +284,23 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_no_success_action() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); - let _m = mock_lnurl_pay_callback_endpoint_no_success_action(LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: None, - pr: Some(inv.to_string()), - comment: comment.clone(), - })?; + mock_lnurl_pay_callback_endpoint_no_success_action( + &mock_rest_client, + LnurlPayCallbackParams { + error: None, + pr: Some(inv.to_string()), + }, + ); - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -431,19 +334,18 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_unsupported_success_action() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let user_amount_msat = 11000; let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); - let _m = - mock_lnurl_pay_callback_endpoint_unsupported_success_action(LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, + + mock_lnurl_pay_callback_endpoint_unsupported_success_action(&mock_rest_client, LnurlPayCallbackParams { error: None, - pr: None, - comment: comment.clone(), - })?; + pr: Some("lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz".to_string()), + }); - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; let r = mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -462,20 +364,23 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_success_payment_hash() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); - let _m = mock_lnurl_pay_callback_endpoint_msg_success_action(LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: None, - pr: Some(inv.to_string()), - comment: comment.clone(), - })?; - - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + + mock_lnurl_pay_callback_endpoint_msg_success_action( + &mock_rest_client, + LnurlPayCallbackParams { + error: None, + pr: Some(inv.to_string()), + }, + ); + + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -497,20 +402,23 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_msg_success_action() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); - let _m = mock_lnurl_pay_callback_endpoint_msg_success_action(LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: None, - pr: Some(inv.to_string()), - comment: comment.clone(), - })?; - - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + + mock_lnurl_pay_callback_endpoint_msg_success_action( + &mock_rest_client, + LnurlPayCallbackParams { + error: None, + pr: Some(inv.to_string()), + }, + ); + + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -547,20 +455,23 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_msg_success_action_incorrect_amount() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap() + 1000; - let _m = mock_lnurl_pay_callback_endpoint_msg_success_action(LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: None, - pr: Some(inv.to_string()), - comment: comment.clone(), - })?; - - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + + mock_lnurl_pay_callback_endpoint_msg_success_action( + &mock_rest_client, + LnurlPayCallbackParams { + error: None, + pr: Some(inv.to_string()), + }, + ); + + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; assert!(mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -578,21 +489,24 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_msg_success_action_error_from_endpoint() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); let expected_error_msg = "Error message from LNURL endpoint"; - let _m = mock_lnurl_pay_callback_endpoint_msg_success_action(LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: Some(expected_error_msg.to_string()), - pr: Some(inv.to_string()), - comment: comment.clone(), - })?; - - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + + mock_lnurl_pay_callback_endpoint_msg_success_action( + &mock_rest_client, + LnurlPayCallbackParams { + error: Some(expected_error_msg.to_string()), + pr: Some(inv.to_string()), + }, + ); + + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; let res = mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -618,23 +532,24 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_url_success_action() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); - let _m = mock_lnurl_pay_callback_endpoint_url_success_action( + + mock_lnurl_pay_callback_endpoint_url_success_action( + &mock_rest_client, LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, error: None, pr: Some(inv.to_string()), - comment: comment.clone(), }, None, - )?; + ); - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -676,23 +591,24 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_url_success_action_validate_url_invalid() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); - let _m = mock_lnurl_pay_callback_endpoint_url_success_action( + + mock_lnurl_pay_callback_endpoint_url_success_action( + &mock_rest_client, LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, error: None, pr: Some(inv.to_string()), - comment: comment.clone(), }, Some("http://different.localhost:8080/test-url"), - )?; + ); - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; let r = mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -711,23 +627,24 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_url_success_action_validate_url_valid() -> Result<()> { + let mock_rest_client = MockRestClient::new(); let comment = rand_string(COMMENT_LENGTH as usize); let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH); let temp_desc = pay_req.metadata_str.clone(); let inv = rand_invoice_with_description_hash(temp_desc)?; let user_amount_msat = inv.amount_milli_satoshis().unwrap(); - let _m = mock_lnurl_pay_callback_endpoint_url_success_action( + + mock_lnurl_pay_callback_endpoint_url_success_action( + &mock_rest_client, LnurlPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, error: None, pr: Some(inv.to_string()), - comment: comment.clone(), }, Some("http://different.localhost:8080/test-url"), - )?; + ); - let mock_breez_services = crate::breez_services::tests::breez_services().await?; + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with(None, Some(rest_client), vec![]).await?; match mock_breez_services .lnurl_pay(LnUrlPayRequest { data: pay_req, @@ -769,6 +686,7 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_aes_success_action() -> Result<()> { + let mock_rest_client = MockRestClient::new(); // Expected fields in the AES payload let description = "test description in AES payload".to_string(); let plaintext = "Hello, test plaintext".to_string(); @@ -794,16 +712,17 @@ pub(crate) mod tests { let user_amount_msat = inv.amount_milli_satoshis().unwrap(); let bolt11 = inv.to_string(); - let _m = mock_lnurl_pay_callback_endpoint_aes_success_action(AesPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: None, - pr: Some(bolt11.clone()), - sa_data: sa_data.clone(), - iv_bytes: random::<[u8; 16]>(), - key_bytes: preimage.into_inner(), - comment: comment.clone(), - })?; + + mock_lnurl_pay_callback_endpoint_aes_success_action( + &mock_rest_client, + AesPayCallbackParams { + error: None, + pr: Some(bolt11.clone()), + sa_data: sa_data.clone(), + iv_bytes: random::<[u8; 16]>(), + key_bytes: preimage.into_inner(), + }, + ); let mock_node_api = MockNodeAPI::new(get_dummy_node_state()); let model_payment = mock_node_api @@ -811,8 +730,10 @@ pub(crate) mod tests { .await?; let known_payments: Vec = vec![model_payment]; - let mock_breez_services = crate::breez_services::tests::breez_services_with( + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with( Some(Arc::new(mock_node_api)), + Some(rest_client), known_payments, ) .await?; @@ -854,6 +775,7 @@ pub(crate) mod tests { #[tokio::test] async fn test_lnurl_pay_aes_success_action_fail_to_decrypt() -> Result<()> { + let mock_rest_client = MockRestClient::new(); // Expected error in the AES payload let sa = SuccessActionProcessed::Aes { result: AesSuccessActionDataResult::ErrorStatus { @@ -880,16 +802,17 @@ pub(crate) mod tests { plaintext, }; let wrong_key = vec![0u8; 32]; - let _m = mock_lnurl_pay_callback_endpoint_aes_success_action(AesPayCallbackParams { - pay_req: &pay_req, - user_amount_msat, - error: None, - pr: Some(bolt11.clone()), - sa_data: sa_data.clone(), - iv_bytes: random::<[u8; 16]>(), - key_bytes: wrong_key.try_into().unwrap(), - comment: comment.clone(), - })?; + + mock_lnurl_pay_callback_endpoint_aes_success_action( + &mock_rest_client, + AesPayCallbackParams { + error: None, + pr: Some(bolt11.clone()), + sa_data: sa_data.clone(), + iv_bytes: random::<[u8; 16]>(), + key_bytes: wrong_key.try_into().unwrap(), + }, + ); let mock_node_api = MockNodeAPI::new(get_dummy_node_state()); let model_payment = mock_node_api @@ -897,8 +820,10 @@ pub(crate) mod tests { .await?; let known_payments: Vec = vec![model_payment]; - let mock_breez_services = crate::breez_services::tests::breez_services_with( + let rest_client: Arc = Arc::new(mock_rest_client); + let mock_breez_services = breez_services_with( Some(Arc::new(mock_node_api)), + Some(rest_client), known_payments, ) .await?; diff --git a/libs/sdk-core/src/swap_out/boltzswap.rs b/libs/sdk-core/src/swap_out/boltzswap.rs index 99c14d268..aed6e0bbc 100644 --- a/libs/sdk-core/src/swap_out/boltzswap.rs +++ b/libs/sdk-core/src/swap_out/boltzswap.rs @@ -1,16 +1,14 @@ use std::collections::HashMap; +use std::sync::Arc; use serde::{Deserialize, Serialize}; use serde_json::to_string_pretty; use const_format::concatcp; -use reqwest::header::CONTENT_TYPE; -use reqwest::Body; use sdk_common::prelude::*; use serde_json::json; use crate::bitcoin::Txid; -use crate::error::SdkError; use crate::models::ReverseSwapPairInfo; use crate::swap_out::reverseswap::CreateReverseSwapResponse; use crate::{ReverseSwapServiceAPI, RouteHint, RouteHintHop}; @@ -218,12 +216,45 @@ pub enum BoltzApiReverseSwapStatus { InvoiceExpired, } -pub struct BoltzApi {} +pub struct BoltzApi { + rest_client: Arc, +} + +impl BoltzApi { + pub fn new(rest_client: Arc) -> Self { + BoltzApi { rest_client } + } + + pub async fn reverse_swap_pair_info(&self) -> ReverseSwapResult { + let (response, _) = + get_and_check_success(self.rest_client.as_ref(), GET_PAIRS_ENDPOINT).await?; + let pairs: Pairs = parse_json(&response)?; + match pairs.pairs.get("BTC/BTC") { + None => Err(ReverseSwapError::generic("BTC pair not found")), + Some(btc_pair) => { + debug!( + "Boltz API pair: {}", + serde_json::to_string_pretty(&btc_pair)? + ); + let hash = String::from(&btc_pair.hash); + Ok(ReverseSwapPairInfo { + fees_hash: hash, + min: btc_pair.limits.minimal, + max: btc_pair.limits.maximal, + fees_percentage: btc_pair.fees.percentage, + fees_lockup: btc_pair.fees.miner_fees.base_asset.reverse.lockup, + fees_claim: btc_pair.fees.miner_fees.base_asset.reverse.claim, + total_fees: None, + }) + } + } + } +} #[tonic::async_trait] impl ReverseSwapServiceAPI for BoltzApi { async fn fetch_reverse_swap_fees(&self) -> ReverseSwapResult { - reverse_swap_pair_info().await + self.reverse_swap_pair_info().await } /// Call Boltz API and parse response as per https://docs.boltz.exchange/en/latest/api/#creating-reverse-swaps @@ -243,30 +274,23 @@ impl ReverseSwapServiceAPI for BoltzApi { pair_hash: String, routing_node: String, ) -> ReverseSwapResult { - get_reqwest_client() - .map_err(SdkError::from)? - .post(CREATE_REVERSE_SWAP_ENDPOINT) - .header(CONTENT_TYPE, "application/json") - .body(build_boltz_reverse_swap_args( - amount_sat, - preimage_hash_hex, - pair_hash.clone(), - claim_pubkey.clone(), - routing_node, - )) - .send() - .await - .map_err(|e| ReverseSwapError::ServiceConnectivity(e.to_string()))? - .text() - .await - .map_err(|e| { + let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]); + let body = build_boltz_reverse_swap_args( + amount_sat, + preimage_hash_hex, + pair_hash.clone(), + claim_pubkey.clone(), + routing_node, + ); + self.rest_client + .post(CREATE_REVERSE_SWAP_ENDPOINT, Some(headers), Some(body)).await.map_err(|e| { ReverseSwapError::ServiceConnectivity(format!( "(Boltz {CREATE_REVERSE_SWAP_ENDPOINT}) Failed to request creation of reverse swap: {e}" )) }) - .and_then(|res| { - trace!("Boltz API create raw response {}", to_string_pretty(&res)?); - serde_json::from_str::(&res).map_err(|e| { + .and_then(|(response, _)| { + trace!("Boltz API create raw response {}", to_string_pretty(&response)?); + serde_json::from_str::(&response).map_err(|e| { ReverseSwapError::ServiceConnectivity(format!( "(Boltz {CREATE_REVERSE_SWAP_ENDPOINT}) Failed to parse create swap response: {e}" )) @@ -284,24 +308,17 @@ impl ReverseSwapServiceAPI for BoltzApi { /// Boltz API errors (e.g. providing an invalid ID arg) are returned as a successful response of /// type [BoltzApiCreateReverseSwapResponse::BoltzApiError] async fn get_boltz_status(&self, id: String) -> ReverseSwapResult { - get_reqwest_client() - .map_err(SdkError::from)? - .post(GET_SWAP_STATUS_ENDPOINT) - .header(CONTENT_TYPE, "application/json") - .body(Body::from(json!({ "id": id }).to_string())) - .send() - .await - .map_err(|e| ReverseSwapError::ServiceConnectivity(e.to_string()))? - .text() - .await - .map_err(|e| { + let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]); + let body = json!({ "id": id }).to_string(); + self.rest_client + .post(GET_SWAP_STATUS_ENDPOINT, Some(headers), Some(body)).await.map_err(|e| { ReverseSwapError::ServiceConnectivity(format!( "(Boltz {GET_SWAP_STATUS_ENDPOINT}) Failed to request swap status: {e}" )) }) - .and_then(|res| { - trace!("Boltz API status raw response {}", to_string_pretty(&res)?); - serde_json::from_str::(&res).map_err(|e| { + .and_then(|(response, _)| { + trace!("Boltz API status raw response {}", to_string_pretty(&response)?); + serde_json::from_str::(&response).map_err(|e| { ReverseSwapError::ServiceConnectivity(format!( "(Boltz {GET_SWAP_STATUS_ENDPOINT}) Failed to parse get status response: {e}" )) @@ -310,29 +327,21 @@ impl ReverseSwapServiceAPI for BoltzApi { } async fn get_route_hints(&self, routing_node_id: String) -> ReverseSwapResult> { - get_reqwest_client() - .map_err(SdkError::from)? - .post(GET_ROUTE_HINTS_ENDPOINT) - .header(CONTENT_TYPE, "application/json") - .body(Body::from( - json!({ "routingNode": routing_node_id, "symbol": "BTC" }).to_string(), - )) - .send() - .await - .map_err(|e| ReverseSwapError::ServiceConnectivity(e.to_string()))? - .text() - .await + let headers = HashMap::from([("Content-Type".to_string(), "application/json".to_string())]); + let body = json!({ "routingNode": routing_node_id, "symbol": "BTC" }).to_string(); + self.rest_client + .post(GET_ROUTE_HINTS_ENDPOINT, Some(headers), Some(body)).await .map_err(|e| { ReverseSwapError::ServiceConnectivity(format!( "(Boltz {GET_ROUTE_HINTS_ENDPOINT}) Failed to get routing hints: {e}" )) }) - .and_then(|res| { + .and_then(|(response, _)| { trace!( "Boltz API routinghints raw response {}", - to_string_pretty(&res)? + to_string_pretty(&response)? ); - serde_json::from_str::(&res) + serde_json::from_str::(&response) .map_err(|e| { ReverseSwapError::ServiceConnectivity(format!( "(Boltz {GET_ROUTE_HINTS_ENDPOINT}) Failed to parse get route hints response: {e}" @@ -343,31 +352,6 @@ impl ReverseSwapServiceAPI for BoltzApi { } } -pub async fn reverse_swap_pair_info() -> ReverseSwapResult { - let pairs: Pairs = get_parse_and_log_response(GET_PAIRS_ENDPOINT, true) - .await - .map_err(SdkError::from)?; - match pairs.pairs.get("BTC/BTC") { - None => Err(ReverseSwapError::generic("BTC pair not found")), - Some(btc_pair) => { - debug!( - "Boltz API pair: {}", - serde_json::to_string_pretty(&btc_pair)? - ); - let hash = String::from(&btc_pair.hash); - Ok(ReverseSwapPairInfo { - fees_hash: hash, - min: btc_pair.limits.minimal, - max: btc_pair.limits.maximal, - fees_percentage: btc_pair.fees.percentage, - fees_lockup: btc_pair.fees.miner_fees.base_asset.reverse.lockup, - fees_claim: btc_pair.fees.miner_fees.base_asset.reverse.claim, - total_fees: None, - }) - } - } -} - fn build_boltz_reverse_swap_args( amount_sat: u64, preimage_hash_hex: String, diff --git a/libs/sdk-core/src/swap_out/error.rs b/libs/sdk-core/src/swap_out/error.rs index 91012b7eb..f0a72be58 100644 --- a/libs/sdk-core/src/swap_out/error.rs +++ b/libs/sdk-core/src/swap_out/error.rs @@ -1,3 +1,5 @@ +use sdk_common::prelude::ServiceConnectivityError; + use crate::{ bitcoin::{hashes, secp256k1}, error::SdkError, @@ -90,6 +92,12 @@ impl From for ReverseSwapError { } } +impl From for ReverseSwapError { + fn from(value: ServiceConnectivityError) -> Self { + Self::ServiceConnectivity(value.err) + } +} + impl From for ReverseSwapError { fn from(value: SdkError) -> Self { match value {