From eb75e48193b0cc1dcd6801ea55e5423761668db1 Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Mon, 17 Jun 2024 17:07:15 +0000 Subject: [PATCH] Extract `sdk-common` crate (#1002) * Extract LnUrlError * Extract sdk-lnurl/auth and sdk-utils * Cargo fmt * Create lnurl module * Consolidate sdk-utils as module in sdk-lnurl * Rename sdk-lnurl workspace member to sdk-common * Move invoice module into sdk-common * Move input_parser module into sdk-common * Move pay module into sdk-common/lnurl * Move withdraw module into sdk-common/lnurl * Move auth structs into lnurl/auth/model * Revert test methods to crate visibility * Re-enable test-utils with FRB v1 workaround * Fix tests, clippy * Fix clippy for tests * Re-generate RN bindings * Fix tests, clippy, FRB * Revert "Fix tests, clippy, FRB" This reverts commit 1964f22f7b7d7f08379bca411d3eb70a2c55e398. * Consolidate pay/withdraw request data structs * Move LnUrlPayError to sdk-common * Make lnurl-pay model public * Re-introduce payment in lnurl-pay result * Move lnurl-pay tests from sdk-common to SDK * Move LNURL-withdraw request, error into sdk-common * Re-generate RN bindings * Potential mockito server fix * Move test_utils.rs in inner module of lib.rs * Fix dart bridge mapping * Fix cargo clippy * Move UDL segments to minimize diff * Remove unused LnUrlPayResult * Potential fix for golang tests * Potential fix for golang tests v2 * Move LnUrlError conversion to lnurl/error.rs * Rename InputType::LnUrlEndpointError back to LnUrlError * Fix tests * Remove Wrapped prefix from LnUrlPaySuccessData, LnUrlPayResult * LNURL-pay: move non-E2E tests to sdk-common * LNURL-withdraw: move non-E2E tests to sdk-common * LNURL-withdraw: move remaining tests to sdk-common (E2E tests, but only cover first portion of the flow, without payment) * fix mirroring definitions * remove test attribute * Don't lint test_utils * run cargo fmt --------- Co-authored-by: Roei Erez --- libs/Cargo.lock | 43 +- libs/Cargo.toml | 31 +- libs/sdk-bindings/Cargo.toml | 13 +- libs/sdk-bindings/src/breez_sdk.udl | 20 +- libs/sdk-bindings/src/uniffi_binding.rs | 8 +- libs/sdk-common/Cargo.toml | 30 + .../src/input_parser.rs | 274 ++------ libs/{sdk-core => sdk-common}/src/invoice.rs | 38 +- libs/sdk-common/src/lib.rs | 23 + .../src/lnurl/error.rs | 29 +- libs/sdk-common/src/lnurl/mod.rs | 46 ++ libs/sdk-common/src/lnurl/model.rs | 29 + .../src/lnurl/specs}/auth.rs | 73 +- libs/sdk-common/src/lnurl/specs/mod.rs | 3 + libs/sdk-common/src/lnurl/specs/pay.rs | 652 ++++++++++++++++++ .../src/lnurl/specs}/withdraw.rs | 156 ++++- libs/sdk-common/src/model.rs | 34 + libs/sdk-common/src/utils/mod.rs | 10 + libs/sdk-common/src/utils/rest_client.rs | 98 +++ libs/sdk-core/Cargo.toml | 44 +- libs/sdk-core/src/binding.rs | 216 +++++- libs/sdk-core/src/breez_services.rs | 58 +- libs/sdk-core/src/bridge_generated.rs | 632 +++++++++++------ libs/sdk-core/src/chain.rs | 18 +- libs/sdk-core/src/error.rs | 167 +---- libs/sdk-core/src/greenlight/node_api.rs | 2 +- libs/sdk-core/src/lib.rs | 21 +- libs/sdk-core/src/lnurl/mod.rs | 48 +- libs/sdk-core/src/lnurl/pay.rs | 582 ++-------------- libs/sdk-core/src/models.rs | 103 +-- libs/sdk-core/src/node_api.rs | 25 +- libs/sdk-core/src/persist/transactions.rs | 33 +- libs/sdk-core/src/swap_out/boltzswap.rs | 16 +- libs/sdk-core/src/test_utils.rs | 4 +- .../ios/Classes/bridge_generated.h | 4 + libs/sdk-flutter/lib/bridge_generated.dart | 140 +--- tools/sdk-cli/Cargo.lock | 28 +- 37 files changed, 2137 insertions(+), 1614 deletions(-) create mode 100644 libs/sdk-common/Cargo.toml rename libs/{sdk-core => sdk-common}/src/input_parser.rs (89%) rename libs/{sdk-core => sdk-common}/src/invoice.rs (92%) create mode 100644 libs/sdk-common/src/lib.rs rename libs/{sdk-core => sdk-common}/src/lnurl/error.rs (74%) create mode 100644 libs/sdk-common/src/lnurl/mod.rs create mode 100644 libs/sdk-common/src/lnurl/model.rs rename libs/{sdk-core/src/lnurl => sdk-common/src/lnurl/specs}/auth.rs (68%) create mode 100644 libs/sdk-common/src/lnurl/specs/mod.rs create mode 100644 libs/sdk-common/src/lnurl/specs/pay.rs rename libs/{sdk-core/src/lnurl => sdk-common/src/lnurl/specs}/withdraw.rs (58%) create mode 100644 libs/sdk-common/src/model.rs create mode 100644 libs/sdk-common/src/utils/mod.rs create mode 100644 libs/sdk-common/src/utils/rest_client.rs diff --git a/libs/Cargo.lock b/libs/Cargo.lock index 88664ad3f..55df7e4d4 100644 --- a/libs/Cargo.lock +++ b/libs/Cargo.lock @@ -539,8 +539,6 @@ dependencies = [ "aes", "anyhow", "base64 0.13.1", - "bip21", - "cbc", "chrono", "const_format", "ecies", @@ -556,13 +554,13 @@ dependencies = [ "once_cell", "openssl", "prost", - "querystring", "rand", "regex", "reqwest", "ripemd", "rusqlite", "rusqlite_migration", + "sdk-common", "serde", "serde_json", "serde_with", @@ -589,6 +587,7 @@ dependencies = [ "glob", "log", "once_cell", + "sdk-common", "thiserror", "tiny-bip39", "tokio", @@ -1949,13 +1948,13 @@ dependencies = [ [[package]] name = "mockito" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d3038e23466858569c2d30a537f691fa0d53b51626630ae08262943e3bbb8b" +checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e" dependencies = [ "assert-json-diff", "colored", - "futures", + "futures-core", "hyper", "log", "rand", @@ -1974,11 +1973,10 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2784,6 +2782,33 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sdk-common" +version = "0.4.1" +dependencies = [ + "aes", + "anyhow", + "base64 0.13.1", + "bip21", + "bitcoin 0.29.2", + "cbc", + "hex", + "lightning", + "lightning-invoice", + "log", + "mockito", + "once_cell", + "querystring", + "regex", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "strum_macros", + "thiserror", + "tokio", +] + [[package]] name = "secp256k1" version = "0.24.3" diff --git a/libs/Cargo.toml b/libs/Cargo.toml index 94ea838a4..78d04355b 100644 --- a/libs/Cargo.toml +++ b/libs/Cargo.toml @@ -7,8 +7,9 @@ rpath = true [workspace] members = [ - "sdk-core", "sdk-bindings", + "sdk-common", + "sdk-core", ] resolver = "2" @@ -16,5 +17,31 @@ resolver = "2" version = "0.4.1" [workspace.dependencies] +aes = "0.8" +anyhow = { version = "1.0.79", features = ["backtrace"] } +base64 = "0.13.0" +bitcoin = "=0.29.2" # Same version as used in gl-client +# Pin the reqwest dependency until macOS linker issue is fixed: https://github.com/seanmonstar/reqwest/issues/2006 +hex = "0.4" +lightning = "=0.0.116" # Same version as used in gl-client +lightning-invoice = "=0.24.0" # Same version as used in gl-client +log = "0.4" +mockito = "1" +once_cell = "1" +regex = "1.8.1" +reqwest = { version = "=0.11.20", features = ["json"] } +rusqlite = { version = "0.29", features = [ + "serde_json", + "bundled", + "backup", + "trace", + "hooks", +] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +strum = "0.25" +strum_macros = "0.25" +thiserror = "1.0.56" +tokio = { version = "1", features = ["full"] } uniffi = "0.23.0" -uniffi_macros = "0.23.0" \ No newline at end of file +uniffi_macros = "0.23.0" diff --git a/libs/sdk-bindings/Cargo.toml b/libs/sdk-bindings/Cargo.toml index b981f9cf5..775462620 100644 --- a/libs/sdk-bindings/Cargo.toml +++ b/libs/sdk-bindings/Cargo.toml @@ -14,17 +14,18 @@ crate-type = ["staticlib", "cdylib", "lib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } breez-sdk-core = { path = "../sdk-core" } -anyhow = { version = "1.0.79", features = ["backtrace"] } -thiserror = "1.0.56" -tokio = { version = "1", features = ["full"] } +sdk-common = { path = "../sdk-common" } +thiserror = { workspace = true } +tokio = { workspace = true } uniffi = { version = "0.23.0", features = ["bindgen-tests", "cli"] } uniffi_bindgen = "0.23.0" uniffi_macros = "0.23.0" uniffi-kotlin-multiplatform = { git = "https://gitlab.com/trixnity/uniffi-kotlin-multiplatform-bindings", rev = "bf48c5fcb153856e3055025a3cbfa56fbf213188" } camino = "1.1.1" -log = "*" -once_cell = "*" +log = { workspace = true } +once_cell = { workspace = true } flutter_rust_bridge = "=1.82.6" tiny-bip39 = "*" tonic = { version = "^0.8", features = [ @@ -36,5 +37,5 @@ tonic = { version = "^0.8", features = [ [build-dependencies] uniffi_build = { version = "0.23.0" } uniffi_bindgen = "0.23.0" -anyhow = { version = "1.0.79", features = ["backtrace"] } +anyhow = { workspace = true } glob = "0.3.1" diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index 0d577beeb..8a0e544a0 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -1,10 +1,10 @@ -dictionary RouteHintHop { +dictionary RouteHintHop { string src_node_id; u64 short_channel_id; u32 fees_base_msat; - u32 fees_proportional_millionths; - u64 cltv_expiry_delta; - u64? htlc_minimum_msat; + u32 fees_proportional_millionths; + u64 cltv_expiry_delta; + u64? htlc_minimum_msat; u64? htlc_maximum_msat; }; @@ -187,7 +187,7 @@ dictionary NodeState { u64 max_single_payment_amount_msat; u64 max_chan_reserve_msats; sequence connected_peers; - u64 inbound_liquidity_msats; + u64 inbound_liquidity_msats; }; dictionary ConfigureNodeRequest { @@ -243,8 +243,8 @@ enum PaymentType { dictionary Payment { string id; - PaymentType payment_type; - i64 payment_time; + PaymentType payment_type; + i64 payment_time; u64 amount_msat; u64 fee_msat; PaymentStatus status; @@ -398,7 +398,7 @@ dictionary PaymentFailedData { }; dictionary BackupFailedData { - string error; + string error; }; [Enum] @@ -409,7 +409,7 @@ interface BreezEvent { PaymentSucceed(Payment details); PaymentFailed(PaymentFailedData details); BackupStarted(); - BackupSucceeded(); + BackupSucceeded(); BackupFailed(BackupFailedData details); SwapUpdated(SwapInfo details); }; @@ -423,7 +423,7 @@ callback interface LogStream { void log(LogEntry l); }; -callback interface EventListener { +callback interface EventListener { void on_event(BreezEvent e); }; diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs index 58e8ef10c..36168d54c 100644 --- a/libs/sdk-bindings/src/uniffi_binding.rs +++ b/libs/sdk-bindings/src/uniffi_binding.rs @@ -1,4 +1,7 @@ +use std::sync::Arc; + use anyhow::Result; +use breez_sdk_core::lnurl::pay::{LnUrlPayResult, LnUrlPaySuccessData}; use breez_sdk_core::{ error::*, mnemonic_to_seed as sdk_mnemonic_to_seed, parse as sdk_parse_input, parse_invoice as sdk_parse_invoice, AesSuccessActionDataDecrypted, AesSuccessActionDataResult, @@ -8,8 +11,8 @@ use breez_sdk_core::{ ConnectRequest, CurrencyInfo, EnvironmentType, EventListener, FeeratePreset, FiatCurrency, GreenlightCredentials, GreenlightDeviceCredentials, GreenlightNodeConfig, HealthCheckStatus, InputType, InvoicePaidDetails, LNInvoice, ListPaymentsRequest, LnPaymentDetails, - LnUrlAuthRequestData, LnUrlCallbackStatus, LnUrlErrorData, LnUrlPayErrorData, LnUrlPayRequest, - LnUrlPayRequestData, LnUrlPayResult, LnUrlPaySuccessData, LnUrlWithdrawRequest, + LnUrlAuthRequestData, LnUrlCallbackStatus, LnUrlErrorData, LnUrlPayError, LnUrlPayErrorData, + LnUrlPayRequest, LnUrlPayRequestData, LnUrlWithdrawError, LnUrlWithdrawRequest, LnUrlWithdrawRequestData, LnUrlWithdrawResult, LnUrlWithdrawSuccessData, LocaleOverrides, LocalizedName, LogEntry, LogStream, LspInformation, MaxReverseSwapAmountResponse, MessageSuccessActionData, MetadataFilter, MetadataItem, Network, NodeConfig, NodeCredentials, @@ -29,7 +32,6 @@ use breez_sdk_core::{ }; use log::{Level, LevelFilter, Metadata, Record}; use once_cell::sync::{Lazy, OnceCell}; -use std::sync::Arc; static RT: Lazy = Lazy::new(|| tokio::runtime::Runtime::new().unwrap()); static LOG_INIT: OnceCell = OnceCell::new(); diff --git a/libs/sdk-common/Cargo.toml b/libs/sdk-common/Cargo.toml new file mode 100644 index 000000000..e1a8b1d6d --- /dev/null +++ b/libs/sdk-common/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "sdk-common" +edition = "2021" +version.workspace = true + +[dependencies] +aes = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +bip21 = "0.2" +bitcoin = { workspace = true } +cbc = { version = "0.1", features = ["std"] } +hex = { workspace = true } +lightning = { workspace = true } +lightning-invoice = { workspace = true } +log = { workspace = true } +querystring = "1" +regex = { workspace = true } +reqwest = { workspace = true } +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +strum_macros = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +bitcoin = { workspace = true, features = ["rand"] } +mockito = { workspace = true } +tokio = { workspace = true } +once_cell = { workspace = true } \ No newline at end of file diff --git a/libs/sdk-core/src/input_parser.rs b/libs/sdk-common/src/input_parser.rs similarity index 89% rename from libs/sdk-core/src/input_parser.rs rename to libs/sdk-common/src/input_parser.rs index 272c98e70..50f2cd7e2 100644 --- a/libs/sdk-core/src/input_parser.rs +++ b/libs/sdk-common/src/input_parser.rs @@ -1,23 +1,13 @@ use std::str::FromStr; -use std::time::Duration; use anyhow::{anyhow, Result}; use bip21::Uri; -use reqwest::StatusCode; -use serde::Deserialize; -use serde::Serialize; - -use crate::bitcoin::bech32; -use crate::bitcoin::bech32::FromBase32; -use crate::ensure_sdk; -use crate::error::SdkError; -use crate::error::SdkError::ServiceConnectivity; -use crate::error::SdkResult; -use crate::input_parser::InputType::*; -use crate::input_parser::LnUrlRequestData::*; -use crate::invoice::{parse_invoice, LNInvoice}; -use crate::lnurl::error::LnUrlResult; -use crate::lnurl::maybe_replace_host_with_mockito_test_host; +use bitcoin::bech32; +use bitcoin::bech32::FromBase32; +use serde::{Deserialize, Serialize}; +use LnUrlRequestData::*; + +use crate::prelude::*; /// Parses generic user input, typically pasted from clipboard or scanned from a QR. /// @@ -26,7 +16,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ## On-chain BTC addresses (incl. BIP 21 URIs) /// /// ``` -/// use breez_sdk_core::{InputType::*, parse}; +/// use sdk_common::prelude::{InputType::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -47,7 +37,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ## BOLT 11 invoices /// /// ``` -/// use breez_sdk_core::{InputType::*, parse}; +/// use sdk_common::prelude::{InputType::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -64,7 +54,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ## Web URLs /// /// ``` -/// use breez_sdk_core::{InputType::*, parse}; +/// use sdk_common::prelude::{InputType::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -77,7 +67,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ### Web URLs with `lightning` query param with an LNURL value. /// /// ```no_run -/// use breez_sdk_core::{InputType::*, parse}; +/// use sdk_common::prelude::{InputType::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -93,7 +83,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ### LNURL pay request /// /// ```no_run -/// use breez_sdk_core::{InputType::*, LnUrlRequestData::*, parse}; +/// use sdk_common::prelude::{InputType::*, LnUrlRequestData::*, parse}; /// use anyhow::Result; /// /// #[tokio::main] @@ -121,7 +111,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ### LNURL withdraw request /// /// ```no_run -/// use breez_sdk_core::{InputType::*, LnUrlRequestData::*, parse}; +/// use sdk_common::prelude::{InputType::*, LnUrlRequestData::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -145,7 +135,7 @@ use crate::lnurl::maybe_replace_host_with_mockito_test_host; /// ### LNURL auth request /// /// ```no_run -/// use breez_sdk_core::{InputType::*, LnUrlRequestData::*, parse}; +/// use sdk_common::prelude::{InputType::*, LnUrlRequestData::*, parse}; /// /// #[tokio::main] /// async fn main() { @@ -178,28 +168,28 @@ pub async fn parse(input: &str) -> Result { } return match invoice_param { - None => Ok(BitcoinAddress { + None => Ok(InputType::BitcoinAddress { address: bitcoin_addr_data, }), - Some(invoice) => Ok(Bolt11 { invoice }), + Some(invoice) => Ok(InputType::Bolt11 { invoice }), }; } if let Ok(invoice) = parse_invoice(input) { - return Ok(Bolt11 { invoice }); + return Ok(InputType::Bolt11 { invoice }); } // Public key serialized in compressed form (66 hex chars) - if let Ok(_node_id) = crate::bitcoin::secp256k1::PublicKey::from_str(input) { - return Ok(NodeId { + if let Ok(_node_id) = bitcoin::secp256k1::PublicKey::from_str(input) { + return Ok(InputType::NodeId { node_id: input.into(), }); } // Possible Node URI (check for separator symbol, try to parse pubkey, ignore rest) if let Some('@') = input.chars().nth(66) { - if let Ok(_node_id) = crate::bitcoin::secp256k1::PublicKey::from_str(&input[..66]) { - return Ok(NodeId { + if let Ok(_node_id) = bitcoin::secp256k1::PublicKey::from_str(&input[..66]) { + return Ok(InputType::NodeId { node_id: input.into(), }); } @@ -215,7 +205,7 @@ pub async fn parse(input: &str) -> Result { return resolve_lnurl(domain, lnurl_endpoint, ln_address).await; } } - return Ok(Url { url: input.into() }); + return Ok(InputType::Url { url: input.into() }); } } @@ -231,73 +221,6 @@ pub async fn parse(input: &str) -> Result { Err(anyhow!("Unrecognized input type")) } -pub(crate) async fn post_and_log_response(url: &str, body: Option) -> SdkResult { - debug!("Making POST request to: {url}"); - - let mut req = get_reqwest_client()?.post(url); - if let Some(body) = body { - req = req.body(body); - } - let raw_body = req - .send() - .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })? - .text() - .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })?; - debug!("Received 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(crate) async fn get_and_log_response(url: &str) -> SdkResult<(String, StatusCode)> { - debug!("Making GET request to: {url}"); - - let response = get_reqwest_client()? - .get(url) - .send() - .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })?; - let status = response.status(); - let raw_body = response - .text() - .await - .map_err(|e| SdkError::ServiceConnectivity { err: e.to_string() })?; - debug!("Received response, status: {status}, raw response body: {raw_body}"); - - Ok((raw_body, status)) -} - -/// 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(crate) async fn get_parse_and_log_response( - url: &str, - enforce_status_check: bool, -) -> SdkResult -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() { - let err = format!("GET request {url} failed with status: {status}"); - error!("{err}"); - return Err(SdkError::ServiceConnectivity { err }); - } - - serde_json::from_str::(&raw_body).map_err(Into::into) -} - /// 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) { @@ -371,18 +294,18 @@ fn lnurl_decode(encoded: &str) -> LnUrlResult<(String, String, Option)> let decoded = String::from_utf8(Vec::from_base32(&payload)?)?; let url = reqwest::Url::parse(&decoded) - .map_err(|e| super::lnurl::error::LnUrlError::InvalidUri(e.to_string()))?; + .map_err(|e| super::prelude::LnUrlError::InvalidUri(e.to_string()))?; let domain = url.domain().ok_or_else(|| { - super::lnurl::error::LnUrlError::invalid_uri("Could not determine domain") + super::prelude::LnUrlError::invalid_uri("Could not determine domain") })?; if url.scheme() == "http" && !domain.ends_with(".onion") { - return Err(super::lnurl::error::LnUrlError::generic( + return Err(super::prelude::LnUrlError::generic( "HTTP scheme only allowed for onion domains", )); } if url.scheme() == "https" && domain.ends_with(".onion") { - return Err(super::lnurl::error::LnUrlError::generic( + return Err(super::prelude::LnUrlError::generic( "HTTPS scheme not allowed for onion domains", )); } @@ -405,13 +328,13 @@ fn lnurl_decode(encoded: &str) -> LnUrlResult<(String, String, Option)> } let url = reqwest::Url::parse(&encoded) - .map_err(|e| super::lnurl::error::LnUrlError::InvalidUri(e.to_string()))?; + .map_err(|e| super::prelude::LnUrlError::InvalidUri(e.to_string()))?; let domain = url.domain().ok_or_else(|| { - super::lnurl::error::LnUrlError::invalid_uri("Could not determine domain") + super::prelude::LnUrlError::invalid_uri("Could not determine domain") })?; ensure_sdk!( supported_prefixes.contains(&url.scheme()), - super::lnurl::error::LnUrlError::generic("Invalid prefix scheme") + super::prelude::LnUrlError::generic("Invalid prefix scheme") ); let scheme = url.scheme(); @@ -433,8 +356,8 @@ async fn resolve_lnurl( // For LNURL-auth links, their type is already known if the link contains the login tag // No need to query the endpoint for details if lnurl_endpoint.contains("tag=login") { - return Ok(LnUrlAuth { - data: crate::lnurl::auth::validate_request(domain, lnurl_endpoint)?, + return Ok(InputType::LnUrlAuth { + data: validate_request(domain, lnurl_endpoint)?, }); } @@ -445,7 +368,7 @@ async fn resolve_lnurl( let temp = lnurl_data.into(); let temp = match temp { // Modify the LnUrlPay payload by adding the domain of the LNURL endpoint - LnUrlPay { data } => LnUrlPay { + InputType::LnUrlPay { data } => InputType::LnUrlPay { data: LnUrlPayRequestData { domain, ln_address, @@ -457,16 +380,8 @@ async fn resolve_lnurl( Ok(temp) } -/// Creates an HTTP client with a built-in connection timeout -pub(crate) fn get_reqwest_client() -> SdkResult { - reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .map_err(|e| ServiceConnectivity { err: e.to_string() }) -} - /// Different kinds of inputs supported by [parse], including any relevant details extracted from the input -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub enum InputType { /// # Supported standards /// @@ -521,6 +436,7 @@ pub enum InputType { data: LnUrlAuthRequestData, }, + /// Error returned by the LNURL endpoint LnUrlError { data: LnUrlErrorData, }, @@ -556,20 +472,14 @@ pub enum LnUrlRequestData { impl From for InputType { fn from(lnurl_data: LnUrlRequestData) -> Self { match lnurl_data { - PayRequest { data } => LnUrlPay { data }, - WithdrawRequest { data } => LnUrlWithdraw { data }, - AuthRequest { data } => LnUrlAuth { data }, - Error { data } => LnUrlError { data }, + PayRequest { data } => Self::LnUrlPay { data }, + WithdrawRequest { data } => Self::LnUrlWithdraw { data }, + AuthRequest { data } => Self::LnUrlAuth { data }, + Error { data } => Self::LnUrlError { data }, } } } -/// Wrapped in a [LnUrlError], this represents a LNURL-endpoint error. -#[derive(Deserialize, Debug, Serialize)] -pub struct LnUrlErrorData { - pub reason: String, -} - /// Wrapped in a [LnUrlPay], this is the result of [parse] when given a LNURL-pay endpoint. /// /// It represents the endpoint's parameters for the LNURL workflow. @@ -638,23 +548,6 @@ impl LnUrlPayRequestData { } } -/// Wrapped in a [LnUrlWithdraw], this is the result of [parse] when given a LNURL-withdraw endpoint. -/// -/// It represents the endpoint's parameters for the LNURL workflow. -/// -/// See -#[derive(Deserialize, Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct LnUrlWithdrawRequestData { - pub callback: String, - pub k1: String, - pub default_description: String, - /// The minimum amount, in millisats, that this LNURL-withdraw endpoint accepts - pub min_withdrawable: u64, - /// The maximum amount, in millisats, that this LNURL-withdraw endpoint accepts - pub max_withdrawable: u64, -} - impl LnUrlWithdrawRequestData { /// The minimum amount, in sats, accepted by this LNURL-withdraw endpoint pub fn min_withdrawable_sats(&self) -> u64 { @@ -667,30 +560,6 @@ impl LnUrlWithdrawRequestData { } } -/// Wrapped in a [LnUrlAuth], this is the result of [parse] when given a LNURL-auth endpoint. -/// -/// It represents the endpoint's parameters for the LNURL workflow. -/// -/// See -#[derive(Deserialize, Debug, Serialize)] -pub struct LnUrlAuthRequestData { - /// Hex encoded 32 bytes of challenge - pub k1: String, - - /// When available, one of: register, login, link, auth - pub action: Option, - - /// Indicates the domain of the LNURL-auth service, to be shown to the user when asking for - /// auth confirmation, as per LUD-04 spec. - #[serde(skip_serializing, skip_deserializing)] - pub domain: String, - - /// Indicates the URL of the LNURL-auth service, including the query arguments. This will be - /// extended with the signed challenge and the linking key, then called in the second step of the workflow. - #[serde(skip_serializing, skip_deserializing)] - pub url: String, -} - /// Key-value pair in the [LnUrlPayRequestData], as returned by the LNURL-pay endpoint #[derive(Deserialize, Debug)] pub struct MetadataItem { @@ -699,10 +568,10 @@ pub struct MetadataItem { } /// Wrapped in a [BitcoinAddress], this is the result of [parse] when given a plain or BIP-21 BTC address. -#[derive(Debug, Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct BitcoinAddressData { pub address: String, - pub network: crate::models::Network, + pub network: super::prelude::Network, pub amount_sat: Option, pub label: Option, pub message: Option, @@ -724,23 +593,28 @@ impl From> for BitcoinAddressData { pub(crate) mod tests { use std::sync::Mutex; - use anyhow::anyhow; - use anyhow::Result; - use mockito::{Mock, Server, ServerGuard}; + use anyhow::{anyhow, Result}; + use bitcoin::bech32; + use bitcoin::bech32::{ToBase32, Variant}; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use mockito::{Mock, Server}; use once_cell::sync::Lazy; - use crate::bitcoin::bech32; - use crate::bitcoin::bech32::{ToBase32, Variant}; - use crate::bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use crate::input_parser::*; - use crate::models::Network; /// 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(|| Mutex::new(Server::new())); + 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) + }); #[tokio::test] async fn test_generic_invalid_input() -> Result<(), Box> { @@ -801,7 +675,7 @@ pub(crate) mod tests { // Address with amount let addr_1 = format!("bitcoin:{addr}?amount=0.00002000"); match parse(&addr_1).await? { - BitcoinAddress { + InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { assert_eq!(addr_with_amount_parsed.address, addr); @@ -817,7 +691,7 @@ pub(crate) mod tests { let label = "test-label"; let addr_2 = format!("bitcoin:{addr}?amount=0.00002000&label={label}"); match parse(&addr_2).await? { - BitcoinAddress { + InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { assert_eq!(addr_with_amount_parsed.address, addr); @@ -833,7 +707,7 @@ pub(crate) mod tests { let message = "test-message"; let addr_3 = format!("bitcoin:{addr}?amount=0.00002000&label={label}&message={message}"); match parse(&addr_3).await? { - BitcoinAddress { + InputType::BitcoinAddress { address: addr_with_amount_parsed, } => { assert_eq!(addr_with_amount_parsed.address, addr); @@ -946,7 +820,7 @@ pub(crate) mod tests { let public_key = PublicKey::from_secret_key(&secp, &secret_key); match parse(&public_key.to_string()).await? { - NodeId { node_id } => { + InputType::NodeId { node_id } => { assert_eq!(node_id, public_key.to_string()); } _ => return Err(anyhow!("Unexpected type")), @@ -1108,7 +982,7 @@ pub(crate) mod tests { ("localhost".into(), format!("https://localhost{path}"), None,) ); - if let LnUrlWithdraw { data: wd } = parse(lnurl_withdraw_encoded).await? { + if let InputType::LnUrlWithdraw { data: wd } = parse(lnurl_withdraw_encoded).await? { assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); assert_eq!( wd.k1, @@ -1134,7 +1008,7 @@ pub(crate) mod tests { ); let url = format!("https://bitcoin.org?lightning={lnurl_withdraw_encoded}"); - if let LnUrlWithdraw { data: wd } = parse(&url).await? { + if let InputType::LnUrlWithdraw { data: wd } = parse(&url).await? { assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); assert_eq!( wd.k1, @@ -1161,7 +1035,7 @@ pub(crate) mod tests { ("localhost".into(), decoded_url.into(), None) ); - if let LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { + if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1173,7 +1047,7 @@ 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 LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { + if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1185,7 +1059,7 @@ 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 LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { + if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1197,7 +1071,7 @@ 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 LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { + if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1209,7 +1083,7 @@ 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 LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { + if let InputType::LnUrlAuth { data: ad } = parse(lnurl_auth_encoded).await? { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1316,7 +1190,7 @@ pub(crate) mod tests { ("localhost".into(), format!("https://localhost{path}"), None) ); - if let LnUrlPay { data: pd } = parse(lnurl_pay_encoded).await? { + if let InputType::LnUrlPay { data: pd } = parse(lnurl_pay_encoded).await? { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1370,7 +1244,7 @@ pub(crate) mod tests { let ln_address = "user@domain.net"; let _m = mock_lnurl_ln_address_endpoint(ln_address, None)?; - if let LnUrlPay { data: pd } = parse(ln_address).await? { + if let InputType::LnUrlPay { data: pd } = parse(ln_address).await? { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1413,7 +1287,7 @@ pub(crate) mod tests { let expected_err = "Error msg from LNURL endpoint found via LN Address"; let _m = mock_lnurl_ln_address_endpoint(ln_address, Some(expected_err.to_string()))?; - if let LnUrlError { data: msg } = parse(ln_address).await? { + if let InputType::LnUrlError { data: msg } = parse(ln_address).await? { assert_eq!(msg.reason, expected_err); return Ok(()); } @@ -1596,7 +1470,7 @@ pub(crate) mod tests { let _m = mock_lnurl_pay_endpoint(pay_path, None); let lnurl_pay_url = format!("lnurlp://localhost{pay_path}"); - if let LnUrlPay { data: pd } = parse(&lnurl_pay_url).await? { + if let InputType::LnUrlPay { data: pd } = parse(&lnurl_pay_url).await? { assert_eq!(pd.callback, "https://localhost/lnurl-pay/callback/db945b624265fc7f5a8d77f269f7589d789a771bdfd20e91a3cf6f50382a98d7"); assert_eq!(pd.max_sendable, 16000); assert_eq!(pd.min_sendable, 4000); @@ -1634,7 +1508,7 @@ pub(crate) mod tests { let withdraw_path = "/lnurl-withdraw?session=e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"; let _m = mock_lnurl_withdraw_endpoint(withdraw_path, None); - if let LnUrlWithdraw { data: wd } = + if let InputType::LnUrlWithdraw { data: wd } = parse(&format!("lnurlw://localhost{withdraw_path}")).await? { assert_eq!(wd.callback, "https://localhost/lnurl-withdraw/callback/e464f841c44dbdd86cee4f09f4ccd3ced58d2e24f148730ec192748317b74538"); @@ -1654,7 +1528,9 @@ pub(crate) mod tests { async fn test_lnurl_auth_lud_17() -> Result<()> { let auth_path = "/lnurl-login?tag=login&k1=1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822"; - if let LnUrlAuth { data: ad } = parse(&format!("keyauth://localhost{auth_path}")).await? { + if let InputType::LnUrlAuth { data: ad } = + parse(&format!("keyauth://localhost{auth_path}")).await? + { assert_eq!( ad.k1, "1a855505699c3e01be41bddd32007bfcc5ff93505dec0cbca64b4b8ff590b822" @@ -1670,7 +1546,9 @@ pub(crate) mod tests { let expected_error_msg = "test pay error"; let _m = mock_lnurl_pay_endpoint(pay_path, Some(expected_error_msg.to_string())); - if let LnUrlError { data: msg } = parse(&format!("lnurlp://localhost{pay_path}")).await? { + if let InputType::LnUrlError { data: msg } = + parse(&format!("lnurlp://localhost{pay_path}")).await? + { assert_eq!(msg.reason, expected_error_msg); return Ok(()); } @@ -1684,7 +1562,7 @@ pub(crate) mod tests { let expected_error_msg = "test withdraw error"; let _m = mock_lnurl_withdraw_endpoint(withdraw_path, Some(expected_error_msg.to_string())); - if let LnUrlError { data: msg } = + if let InputType::LnUrlError { data: msg } = parse(&format!("lnurlw://localhost{withdraw_path}")).await? { assert_eq!(msg.reason, expected_error_msg); diff --git a/libs/sdk-core/src/invoice.rs b/libs/sdk-common/src/invoice.rs similarity index 92% rename from libs/sdk-core/src/invoice.rs rename to libs/sdk-common/src/invoice.rs index b23d1f5e6..04dac480f 100644 --- a/libs/sdk-core/src/invoice.rs +++ b/libs/sdk-common/src/invoice.rs @@ -1,15 +1,15 @@ use std::str::FromStr; use std::time::{SystemTimeError, UNIX_EPOCH}; +use bitcoin::secp256k1::{self, PublicKey}; use hex::ToHex; +use lightning::routing::gossip::RoutingFees; +use lightning::routing::*; +use lightning_invoice::*; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::bitcoin::secp256k1::{self, PublicKey}; -use crate::lightning::routing::gossip::RoutingFees; -use crate::lightning::routing::*; -use crate::lightning_invoice::*; -use crate::Network; +use crate::prelude::*; pub type InvoiceResult = Result; @@ -26,33 +26,33 @@ pub enum InvoiceError { } impl InvoiceError { - pub(crate) fn generic(err: &str) -> Self { + pub fn generic(err: &str) -> Self { Self::Generic(err.to_string()) } - pub(crate) fn invalid_network(err: &str) -> Self { + pub fn invalid_network(err: &str) -> Self { Self::InvalidNetwork(err.to_string()) } - pub(crate) fn validation(err: &str) -> Self { + pub fn validation(err: &str) -> Self { Self::Validation(err.to_string()) } } -impl From for InvoiceError { - fn from(err: crate::lightning_invoice::CreationError) -> Self { +impl From for InvoiceError { + fn from(err: CreationError) -> Self { Self::Generic(err.to_string()) } } -impl From for InvoiceError { - fn from(err: crate::lightning_invoice::Bolt11ParseError) -> Self { +impl From for InvoiceError { + fn from(err: Bolt11ParseError) -> Self { Self::Validation(err.to_string()) } } -impl From for InvoiceError { - fn from(err: crate::lightning_invoice::Bolt11SemanticError) -> Self { +impl From for InvoiceError { + fn from(err: Bolt11SemanticError) -> Self { Self::Validation(err.to_string()) } } @@ -93,7 +93,7 @@ pub struct LNInvoice { } impl LNInvoice { - pub(crate) fn contains_hint_for_node(&self, pubkey: &str) -> bool { + pub fn contains_hint_for_node(&self, pubkey: &str) -> bool { self.routing_hints .iter() .any(|hint| hint.hops.iter().any(|hop| hop.src_node_id == pubkey)) @@ -307,7 +307,7 @@ mod tests { .unwrap(); let mut private_key: [u8; 32] = Default::default(); private_key.copy_from_slice(&private_key_vec[0..32]); - let hint_hop = crate::RouteHintHop { + let hint_hop = self::RouteHintHop { src_node_id: res.payee_pubkey, short_channel_id: 1234, cltv_expiry_delta: 2000, @@ -316,7 +316,7 @@ mod tests { fees_base_msat: 1000, fees_proportional_millionths: 100, }; - let route_hint = crate::RouteHint { + let route_hint = self::RouteHint { hops: vec![hint_hop], }; let encoded = add_routing_hints(&payreq, true, &vec![route_hint], Some(100)).unwrap(); @@ -334,7 +334,7 @@ mod tests { .unwrap(); let mut private_key: [u8; 32] = Default::default(); private_key.copy_from_slice(&private_key_vec[0..32]); - let hint_hop = crate::RouteHintHop { + let hint_hop = self::RouteHintHop { src_node_id: res.payee_pubkey, short_channel_id: 1234, fees_base_msat: 1000, @@ -343,7 +343,7 @@ mod tests { htlc_minimum_msat: Some(3000), htlc_maximum_msat: Some(4000), }; - let route_hint = crate::RouteHint { + let route_hint = self::RouteHint { hops: vec![hint_hop], }; let encoded = add_routing_hints(&payreq, false, &vec![route_hint], Some(100)).unwrap(); diff --git a/libs/sdk-common/src/lib.rs b/libs/sdk-common/src/lib.rs new file mode 100644 index 000000000..4d6ee94bc --- /dev/null +++ b/libs/sdk-common/src/lib.rs @@ -0,0 +1,23 @@ +pub mod input_parser; +pub mod invoice; +mod lnurl; +mod model; +mod utils; + +#[rustfmt::skip] +pub mod prelude { + pub use crate::*; + pub use crate::input_parser::*; + pub use crate::invoice::*; + pub use crate::lnurl::error::*; + pub use crate::lnurl::model::*; + pub use crate::lnurl::specs::auth::model::*; + pub use crate::lnurl::specs::auth::*; + pub use crate::lnurl::specs::pay::model::*; + pub use crate::lnurl::specs::pay::*; + pub use crate::lnurl::specs::withdraw::model::*; + pub use crate::lnurl::specs::withdraw::*; + pub use crate::lnurl::*; + pub use crate::model::*; + pub use crate::utils::rest_client::*; +} diff --git a/libs/sdk-core/src/lnurl/error.rs b/libs/sdk-common/src/lnurl/error.rs similarity index 74% rename from libs/sdk-core/src/lnurl/error.rs rename to libs/sdk-common/src/lnurl/error.rs index 4169b147a..9ccaac8c5 100644 --- a/libs/sdk-core/src/lnurl/error.rs +++ b/libs/sdk-common/src/lnurl/error.rs @@ -1,7 +1,8 @@ use std::{array::TryFromSliceError, string::FromUtf8Error}; -use crate::bitcoin::{bech32, secp256k1, util::bip32}; -use crate::{invoice::InvoiceError, node_api::NodeError}; +use bitcoin::{bech32, secp256k1, util::bip32}; + +use crate::prelude::InvoiceError; pub type LnUrlResult = Result; @@ -10,8 +11,8 @@ pub enum LnUrlError { #[error("{0}")] Generic(String), - #[error(transparent)] - InvalidInvoice(#[from] InvoiceError), + #[error("{0}")] + InvalidInvoice(String), #[error("{0}")] InvalidUri(String), @@ -21,11 +22,11 @@ pub enum LnUrlError { } impl LnUrlError { - pub(crate) fn generic(err: &str) -> Self { + pub fn generic(err: &str) -> Self { Self::Generic(err.to_string()) } - pub(crate) fn invalid_uri(err: &str) -> Self { + pub fn invalid_uri(err: &str) -> Self { Self::InvalidUri(err.to_string()) } } @@ -66,16 +67,6 @@ impl From for LnUrlError { } } -impl From for LnUrlError { - fn from(value: NodeError) -> Self { - match value { - NodeError::InvalidInvoice(err) => Self::InvalidInvoice(err), - NodeError::ServiceConnectivity(err) => Self::ServiceConnectivity(err), - _ => Self::Generic(value.to_string()), - } - } -} - impl From for LnUrlError { fn from(err: secp256k1::Error) -> Self { Self::Generic(err.to_string()) @@ -93,3 +84,9 @@ impl From for LnUrlError { Self::Generic(err.to_string()) } } + +impl From for LnUrlError { + fn from(value: InvoiceError) -> Self { + LnUrlError::InvalidInvoice(format!("{value}")) + } +} diff --git a/libs/sdk-common/src/lnurl/mod.rs b/libs/sdk-common/src/lnurl/mod.rs new file mode 100644 index 000000000..e9e679372 --- /dev/null +++ b/libs/sdk-common/src/lnurl/mod.rs @@ -0,0 +1,46 @@ +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_mockito_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 server = crate::input_parser::tests::MOCK_HTTP_SERVER.lock().unwrap(); + let mockito_endpoint_url = + reqwest::Url::parse(&server.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(mockito_endpoint_url.host_str()) + .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; + let _ = parsed_lnurl_endpoint.set_scheme(mockito_endpoint_url.scheme()); + let _ = parsed_lnurl_endpoint.set_port(mockito_endpoint_url.port()); + + Ok(parsed_lnurl_endpoint.to_string()) +} + +#[cfg(not(test))] +pub(crate) fn maybe_replace_host_with_mockito_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; + use bitcoin::secp256k1::rand::distributions::{Alphanumeric, DistString}; + + pub fn rand_string(len: usize) -> String { + Alphanumeric.sample_string(&mut rand::thread_rng(), len) + } +} diff --git a/libs/sdk-common/src/lnurl/model.rs b/libs/sdk-common/src/lnurl/model.rs new file mode 100644 index 000000000..99378b8eb --- /dev/null +++ b/libs/sdk-common/src/lnurl/model.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +/// Contains the result of the entire LNURL interaction, as reported by the LNURL endpoint. +/// +/// * `Ok` indicates the interaction with the endpoint was valid, and the endpoint +/// - started to pay the invoice asynchronously in the case of LNURL-withdraw, +/// - verified the client signature in the case of LNURL-auth,////// * `Error` indicates a generic issue the LNURL endpoint encountered, including a freetext +/// description of the reason. +/// +/// Both cases are described in LUD-03 & LUD-04: +#[derive(Clone, Deserialize, Debug, Serialize)] +#[serde(rename_all = "UPPERCASE")] +#[serde(tag = "status")] +pub enum LnUrlCallbackStatus { + /// On-wire format is: `{"status": "OK"}` + Ok, + /// On-wire format is: `{"status": "ERROR", "reason": "error details..."}` + #[serde(rename = "ERROR")] + ErrorStatus { + #[serde(flatten)] + data: LnUrlErrorData, + }, +} + +/// Wrapped in a [LnUrlError], this represents a LNURL-endpoint error. +#[derive(Clone, Deserialize, Debug, Serialize)] +pub struct LnUrlErrorData { + pub reason: String, +} diff --git a/libs/sdk-core/src/lnurl/auth.rs b/libs/sdk-common/src/lnurl/specs/auth.rs similarity index 68% rename from libs/sdk-core/src/lnurl/auth.rs rename to libs/sdk-common/src/lnurl/specs/auth.rs index 32e352a86..719d5c4c2 100644 --- a/libs/sdk-core/src/lnurl/auth.rs +++ b/libs/sdk-common/src/lnurl/specs/auth.rs @@ -1,28 +1,24 @@ use std::str::FromStr; -use std::sync::Arc; +use bitcoin::hashes::{hex::ToHex, sha256, Hash, HashEngine, Hmac, HmacEngine}; +use bitcoin::secp256k1::{Message, Secp256k1}; +use bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; +use bitcoin::KeyPair; use reqwest::Url; -use crate::bitcoin::hashes::{hex::ToHex, sha256, Hash, HashEngine, Hmac, HmacEngine}; -use crate::bitcoin::secp256k1::{Message, Secp256k1}; -use crate::bitcoin::util::bip32::ChildNumber; -use crate::bitcoin::KeyPair; -use crate::input_parser::get_parse_and_log_response; -use crate::{node_api::NodeAPI, LnUrlAuthRequestData, LnUrlCallbackStatus}; - -use super::error::{LnUrlError, LnUrlResult}; +use crate::prelude::*; /// Performs the third and last step of LNURL-auth, as per /// /// +/// Linking key is derived as per LUD-05 +/// https://github.com/lnurl/luds/blob/luds/05.md +/// /// See the [parse] docs for more detail on the full workflow. -pub(crate) async fn perform_lnurl_auth( - node_api: Arc, +pub async fn perform_lnurl_auth( + linking_keys: KeyPair, req_data: LnUrlAuthRequestData, ) -> LnUrlResult { - let url = Url::from_str(&req_data.url).map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - let linking_keys = derive_linking_keys(node_api, url)?; - let k1_to_sign = Message::from_slice( &hex::decode(req_data.k1) .map_err(|e| LnUrlError::Generic(format!("Error decoding k1: {e}")))?, @@ -44,7 +40,7 @@ pub(crate) async fn perform_lnurl_auth( .map_err(|e| LnUrlError::ServiceConnectivity(e.to_string())) } -pub(crate) fn validate_request( +pub fn validate_request( domain: String, lnurl_endpoint: String, ) -> LnUrlResult { @@ -91,31 +87,24 @@ fn hmac_sha256(key: &[u8], input: &[u8]) -> Hmac { Hmac::::from_engine(engine) } -/// Linking key is derived as per LUD-05 -/// -/// https://github.com/lnurl/luds/blob/luds/05.md -fn derive_linking_keys(node_api: Arc, url: Url) -> LnUrlResult { +pub fn get_derivation_path( + hashing_key: ExtendedPrivKey, + url: Url, +) -> LnUrlResult> { let domain = url .domain() .ok_or(LnUrlError::invalid_uri("Could not determine domain"))?; - // m/138'/0 - let hashing_key = node_api.derive_bip32_key(vec![ - ChildNumber::from_hardened_idx(138)?, - ChildNumber::from(0), - ])?; let hmac = hmac_sha256(&hashing_key.to_priv().to_bytes(), domain.as_bytes()); // m/138'//// - let linking_key = node_api.derive_bip32_key(vec![ + Ok(vec![ ChildNumber::from_hardened_idx(138)?, ChildNumber::from(build_path_element_u32(hmac[0..4].try_into()?)), ChildNumber::from(build_path_element_u32(hmac[4..8].try_into()?)), ChildNumber::from(build_path_element_u32(hmac[8..12].try_into()?)), ChildNumber::from(build_path_element_u32(hmac[12..16].try_into()?)), - ])?; - - Ok(linking_key.to_keypair(&Secp256k1::new())) + ]) } fn build_path_element_u32(hmac_bytes: [u8; 4]) -> u32 { @@ -123,3 +112,31 @@ fn build_path_element_u32(hmac_bytes: [u8; 4]) -> u32 { buf[..4].copy_from_slice(&hmac_bytes); u32::from_be_bytes(buf) } + +pub mod model { + use serde::{Deserialize, Serialize}; + + /// Wrapped in a [LnUrlAuth], this is the result of [parse] when given a LNURL-auth endpoint. + /// + /// It represents the endpoint's parameters for the LNURL workflow. + /// + /// See + #[derive(Clone, Deserialize, Debug, Serialize)] + pub struct LnUrlAuthRequestData { + /// Hex encoded 32 bytes of challenge + pub k1: String, + + /// When available, one of: register, login, link, auth + pub action: Option, + + /// Indicates the domain of the LNURL-auth service, to be shown to the user when asking for + /// auth confirmation, as per LUD-04 spec. + #[serde(skip_serializing, skip_deserializing)] + pub domain: String, + + /// Indicates the URL of the LNURL-auth service, including the query arguments. This will be + /// extended with the signed challenge and the linking key, then called in the second step of the workflow. + #[serde(skip_serializing, skip_deserializing)] + pub url: String, + } +} diff --git a/libs/sdk-common/src/lnurl/specs/mod.rs b/libs/sdk-common/src/lnurl/specs/mod.rs new file mode 100644 index 000000000..34ca1a8ad --- /dev/null +++ b/libs/sdk-common/src/lnurl/specs/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod auth; +pub(crate) mod pay; +pub(crate) mod withdraw; diff --git a/libs/sdk-common/src/lnurl/specs/pay.rs b/libs/sdk-common/src/lnurl/specs/pay.rs new file mode 100644 index 000000000..468048de1 --- /dev/null +++ b/libs/sdk-common/src/lnurl/specs/pay.rs @@ -0,0 +1,652 @@ +use std::str::FromStr; + +use crate::prelude::*; + +pub type Aes256CbcEnc = cbc::Encryptor; +pub type Aes256CbcDec = cbc::Decryptor; + +/// Validates invoice and performs the second and last step of LNURL-pay, as per +/// +/// +/// See the [parse] docs for more detail on the full workflow. +pub async fn validate_lnurl_pay( + user_amount_msat: u64, + comment: &Option, + req_data: &LnUrlPayRequestData, + network: Network, +) -> LnUrlResult { + validate_user_input( + user_amount_msat, + comment, + req_data.min_sendable, + req_data.max_sendable, + req_data.comment_allowed, + )?; + + 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) { + Ok(ValidatedCallbackResponse::EndpointError { data: err }) + } else { + let callback_resp: CallbackResponse = serde_json::from_str(&callback_resp_text)?; + if let Some(ref sa) = callback_resp.success_action { + match sa { + SuccessAction::Aes(data) => data.validate()?, + SuccessAction::Message(data) => data.validate()?, + SuccessAction::Url(data) => data.validate(req_data)?, + } + } + + validate_invoice(user_amount_msat, &callback_resp.pr, network)?; + Ok(ValidatedCallbackResponse::EndpointSuccess { + data: callback_resp, + }) + } +} + +pub fn build_pay_callback_url( + user_amount_msat: u64, + user_comment: &Option, + data: &LnUrlPayRequestData, +) -> LnUrlResult { + let amount_msat = user_amount_msat.to_string(); + let mut url = reqwest::Url::from_str(&data.callback) + .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; + + url.query_pairs_mut().append_pair("amount", &amount_msat); + if let Some(comment) = user_comment { + url.query_pairs_mut().append_pair("comment", comment); + } + + Ok(url.to_string()) +} + +pub fn validate_user_input( + user_amount_msat: u64, + comment: &Option, + condition_min_amount_msat: u64, + condition_max_amount_msat: u64, + condition_max_comment_len: u16, +) -> LnUrlResult<()> { + ensure_sdk!( + user_amount_msat >= condition_min_amount_msat, + LnUrlError::generic("Amount is smaller than the minimum allowed") + ); + + ensure_sdk!( + user_amount_msat <= condition_max_amount_msat, + LnUrlError::generic("Amount is bigger than the maximum allowed") + ); + + match comment { + None => Ok(()), + Some(msg) => match msg.len() <= condition_max_comment_len as usize { + true => Ok(()), + false => Err(LnUrlError::generic( + "Comment is longer than the maximum allowed comment length", + )), + }, + } +} + +pub fn validate_invoice(user_amount_msat: u64, bolt11: &str, network: Network) -> LnUrlResult<()> { + let invoice = parse_invoice(bolt11)?; + // Valid the invoice network against the config network + validate_network(invoice.clone(), network)?; + + match invoice.amount_msat { + None => Err(LnUrlError::generic( + "Amount is bigger than the maximum allowed", + )), + Some(invoice_amount_msat) => match invoice_amount_msat == user_amount_msat { + true => Ok(()), + false => Err(LnUrlError::generic( + "Invoice amount is different than the user chosen amount", + )), + }, + } +} + +pub mod model { + use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; + use anyhow::Result; + use rusqlite::types::{FromSql, FromSqlError, ToSqlOutput}; + use rusqlite::ToSql; + use serde::{Deserialize, Serialize}; + use thiserror::Error; + + use crate::prelude::specs::pay::{Aes256CbcDec, Aes256CbcEnc}; + use crate::prelude::*; + // use crate::Payment; + + /// Represents a LNURL-pay request. + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct LnUrlPayRequest { + /// The [LnUrlPayRequestData] returned by [crate::input_parser::parse] + pub data: LnUrlPayRequestData, + /// The amount in millisatoshis for this payment + pub amount_msat: u64, + /// An optional comment for this payment + pub comment: Option, + /// The external label or identifier of the [Payment] + pub payment_label: Option, + } + + pub enum ValidatedCallbackResponse { + EndpointSuccess { data: CallbackResponse }, + EndpointError { data: LnUrlErrorData }, + } + + #[derive(Clone, Serialize, Deserialize, Debug)] + pub struct LnUrlPayErrorData { + pub payment_hash: String, + pub reason: String, + } + + #[derive(Clone, Serialize, Deserialize, Debug)] + pub struct LnUrlPaySuccessData { + pub success_action: Option, + } + + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + pub struct CallbackResponse { + pub pr: String, + pub success_action: Option, + } + + /// Payload of the AES success action, as received from the LNURL endpoint + /// + /// See [AesSuccessActionDataDecrypted] for a similar wrapper containing the decrypted payload + #[derive(Deserialize, Debug)] + pub struct AesSuccessActionData { + /// Contents description, up to 144 characters + pub description: String, + + /// Base64, AES-encrypted data where encryption key is payment preimage, up to 4kb of characters + pub ciphertext: String, + + /// Base64, initialization vector, exactly 24 characters + pub iv: String, + } + + /// Result of decryption of [AesSuccessActionData] payload + #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] + pub enum AesSuccessActionDataResult { + Decrypted { data: AesSuccessActionDataDecrypted }, + ErrorStatus { reason: String }, + } + + /// Wrapper for the decrypted [AesSuccessActionData] payload + #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] + pub struct AesSuccessActionDataDecrypted { + /// Contents description, up to 144 characters + pub description: String, + + /// Decrypted content + pub plaintext: String, + } + + #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] + pub struct MessageSuccessActionData { + pub message: String, + } + + #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] + pub struct UrlSuccessActionData { + pub description: String, + pub url: String, + } + + /// [SuccessAction] where contents are ready to be consumed by the caller + /// + /// Contents are identical to [SuccessAction], except for AES where the ciphertext is decrypted. + #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] + pub enum SuccessActionProcessed { + /// See [SuccessAction::Aes] for received payload + /// + /// See [AesSuccessActionDataDecrypted] for decrypted payload + Aes { result: AesSuccessActionDataResult }, + + /// See [SuccessAction::Message] + Message { data: MessageSuccessActionData }, + + /// See [SuccessAction::Url] + Url { data: UrlSuccessActionData }, + } + + impl FromSql for SuccessActionProcessed { + fn column_result( + value: rusqlite::types::ValueRef<'_>, + ) -> rusqlite::types::FromSqlResult { + serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType) + } + } + + impl ToSql for SuccessActionProcessed { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::from( + serde_json::to_string(&self).map_err(|_| FromSqlError::InvalidType)?, + )) + } + } + + /// Supported success action types + /// + /// Receiving any other (unsupported) success action type will result in a failed parsing, + /// which will abort the LNURL-pay workflow, as per LUD-09. + #[derive(Deserialize, Debug)] + #[serde(rename_all = "camelCase")] + #[serde(tag = "tag")] + pub enum SuccessAction { + /// AES type, described in LUD-10 + Aes(AesSuccessActionData), + + /// Message type, described in LUD-09 + Message(MessageSuccessActionData), + + /// URL type, described in LUD-09 + Url(UrlSuccessActionData), + } + + impl AesSuccessActionData { + /// Validates the fields, but does not decrypt and validate the ciphertext. + pub fn validate(&self) -> LnUrlResult<()> { + ensure_sdk!( + self.description.len() <= 144, + LnUrlError::generic( + "AES action description length is larger than the maximum allowed" + ) + ); + + ensure_sdk!( + self.ciphertext.len() <= 4096, + LnUrlError::generic( + "AES action ciphertext length is larger than the maximum allowed" + ) + ); + + base64::decode(&self.ciphertext)?; + + ensure_sdk!( + self.iv.len() == 24, + LnUrlError::generic("AES action iv has unexpected length") + ); + + base64::decode(&self.iv)?; + + Ok(()) + } + + /// Decrypts the ciphertext as a UTF-8 string, given the key (invoice preimage) parameter. + pub fn decrypt(&self, key: &[u8; 32]) -> Result { + let plaintext_bytes = + Aes256CbcDec::new_from_slices(key, &base64::decode(&self.iv)?)? + .decrypt_padded_vec_mut::(&base64::decode(&self.ciphertext)?)?; + + Ok(String::from_utf8(plaintext_bytes)?) + } + + /// Helper method that encrypts a given plaintext, with a given key and IV. + pub fn encrypt(key: &[u8; 32], iv: &[u8; 16], plaintext: String) -> Result { + let ciphertext_bytes = Aes256CbcEnc::new_from_slices(key, iv)? + .encrypt_padded_vec_mut::(plaintext.as_bytes()); + + Ok(base64::encode(ciphertext_bytes)) + } + } + + impl TryFrom<(AesSuccessActionData, &[u8; 32])> for AesSuccessActionDataDecrypted { + type Error = anyhow::Error; + + fn try_from( + value: (AesSuccessActionData, &[u8; 32]), + ) -> std::result::Result { + let data = value.0; + let key = value.1; + + Ok(AesSuccessActionDataDecrypted { + description: data.description.clone(), + plaintext: data.decrypt(key)?, + }) + } + } + + impl MessageSuccessActionData { + pub fn validate(&self) -> LnUrlResult<()> { + match self.message.len() <= 144 { + true => Ok(()), + false => Err(LnUrlError::generic( + "Success action message is longer than the maximum allowed length", + )), + } + } + } + + impl UrlSuccessActionData { + pub fn validate(&self, data: &LnUrlPayRequestData) -> LnUrlResult<()> { + match self.description.len() <= 144 { + true => Ok(()), + false => Err(LnUrlError::generic( + "Success action description is longer than the maximum allowed length", + )), + } + .and_then(|_| { + let req_url = reqwest::Url::parse(&data.callback) + .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; + let req_domain = req_url.domain().ok_or_else(|| { + LnUrlError::invalid_uri("Could not determine callback domain") + })?; + + let action_res_url = reqwest::Url::parse(&self.url) + .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; + let action_res_domain = action_res_url.domain().ok_or_else(|| { + LnUrlError::invalid_uri("Could not determine Success Action URL domain") + })?; + + match req_domain == action_res_domain { + true => Ok(()), + false => Err(LnUrlError::generic( + "Success Action URL has different domain than the callback domain", + )), + } + }) + } + } + + /// Error returned by [crate::breez_services::BreezServices::lnurl_pay] + #[derive(Clone, Debug, Error)] + pub enum LnUrlPayError { + /// This error is raised when attempting to pay an invoice that has already being paid. + #[error("Invoice already paid")] + AlreadyPaid, + + /// This error is raised when a general error occurs not specific to other error variants + /// in this enum. + #[error("Generic: {err}")] + Generic { err: String }, + + /// This error is raised when the amount from the parsed invoice is not set. + #[error("Invalid amount: {err}")] + InvalidAmount { err: String }, + + /// This error is raised when the lightning invoice cannot be parsed. + #[error("Invalid invoice: {err}")] + InvalidInvoice { err: String }, + + /// This error is raised when the lightning invoice is for a different Bitcoin network. + #[error("Invalid network: {err}")] + InvalidNetwork { err: String }, + + /// This error is raised when the decoded LNURL URI is not compliant to the specification. + #[error("Invalid uri: {err}")] + InvalidUri { err: String }, + + /// This error is raised when the lightning invoice has passed it's expiry time. + #[error("Invoice expired: {err}")] + InvoiceExpired { err: String }, + + /// This error is raised when attempting to make a payment by the node fails. + #[error("Payment failed: {err}")] + PaymentFailed { err: String }, + + /// This error is raised when attempting to make a payment takes too long. + #[error("Payment timeout: {err}")] + PaymentTimeout { err: String }, + + /// This error is raised when no route can be found when attempting to make a + /// payment by the node. + #[error("Route not found: {err}")] + RouteNotFound { err: String }, + + /// This error is raised when the route is considered too expensive when + /// attempting to make a payment by the node. + #[error("Route too expensive: {err}")] + RouteTooExpensive { err: String }, + + /// This error is raised when a connection to an external service fails. + #[error("Service connectivity: {err}")] + ServiceConnectivity { err: String }, + } + + impl From for LnUrlPayError { + fn from(err: anyhow::Error) -> Self { + Self::Generic { + err: err.to_string(), + } + } + } + + impl From for LnUrlPayError { + fn from(err: bitcoin::hashes::hex::Error) -> Self { + Self::Generic { + err: err.to_string(), + } + } + } + + impl From for LnUrlPayError { + fn from(value: InvoiceError) -> Self { + match value { + InvoiceError::InvalidNetwork(err) => Self::InvalidNetwork { err }, + InvoiceError::Validation(err) => Self::InvalidInvoice { err }, + InvoiceError::Generic(err) => Self::Generic { err }, + } + } + } + + impl From for LnUrlPayError { + fn from(value: LnUrlError) -> Self { + match value { + LnUrlError::InvalidUri(err) => Self::InvalidUri { err }, + LnUrlError::InvalidInvoice(err) => Self::InvalidInvoice { err }, + LnUrlError::ServiceConnectivity(err) => Self::ServiceConnectivity { err }, + _ => Self::Generic { + err: value.to_string(), + }, + } + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; + use anyhow::Result; + use bitcoin::hashes::{sha256, Hash}; + + use crate::lnurl::specs::pay::*; + use crate::lnurl::tests::rand_string; + + fn get_test_pay_req_data( + min_sendable: u64, + max_sendable: u64, + comment_len: u16, + ) -> LnUrlPayRequestData { + LnUrlPayRequestData { + min_sendable, + max_sendable, + comment_allowed: comment_len, + metadata_str: "".into(), + callback: "http://localhost:8080/callback".into(), + domain: "localhost".into(), + allows_nostr: false, + nostr_pubkey: None, + ln_address: None, + } + } + + #[test] + fn test_lnurl_pay_validate_input() -> Result<()> { + assert!(validate_user_input(100_000, &None, 0, 100_000, 0).is_ok()); + assert!(validate_user_input(100_000, &Some("test".into()), 0, 100_000, 5).is_ok()); + + assert!(validate_user_input(5000, &None, 10_000, 100_000, 5).is_err()); + assert!(validate_user_input(200_000, &None, 10_000, 100_000, 5).is_err()); + assert!(validate_user_input(100_000, &Some("test".into()), 10_000, 100_000, 0).is_err()); + + Ok(()) + } + + #[test] + fn test_lnurl_pay_validate_success_action_encrypt_decrypt() -> Result<()> { + // Simulate a preimage, which will be the AES key + let key = sha256::Hash::hash(&[0x42; 16]); + let key_bytes = key.as_inner(); + + let iv_bytes = [0x24; 16]; // 16 bytes = 24 chars + let iv_base64 = base64::encode(iv_bytes); // JCQkJCQkJCQkJCQkJCQkJA== + + let plaintext = "hello world! this is my plaintext."; + let plaintext_bytes = plaintext.as_bytes(); + + // hex = 91239ab5d94369a18474ee58372c7d0fcee5e227903f671bfe19ef32f1cada804d10f0f006265289d936317343dbc0ca + // base64 = kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK + let ciphertext_bytes = + &hex::decode("91239ab5d94369a18474ee58372c7d0fcee5e227903f671bfe19ef32f1cada804d10f0f006265289d936317343dbc0ca")?; + let ciphertext_base64 = base64::encode(ciphertext_bytes); + + // Encrypt raw (which returns raw bytes) + let res = Aes256CbcEnc::new_from_slices(key_bytes, &iv_bytes)? + .encrypt_padded_vec_mut::(plaintext_bytes); + assert_eq!(res[..], ciphertext_bytes[..]); + + // Decrypt raw (which returns raw bytes) + let res = Aes256CbcDec::new_from_slices(key_bytes, &iv_bytes)? + .decrypt_padded_vec_mut::(&res)?; + assert_eq!(res[..], plaintext_bytes[..]); + + // Encrypt via AesSuccessActionData helper method (which returns a base64 representation of the bytes) + let res = AesSuccessActionData::encrypt(key_bytes, &iv_bytes, plaintext.into())?; + assert_eq!(res, base64::encode(ciphertext_bytes)); + + // Decrypt via AesSuccessActionData instance method (which returns an UTF-8 string of the plaintext bytes) + let res = AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: ciphertext_base64, + iv: iv_base64, + } + .decrypt(key_bytes)?; + assert_eq!(res.as_bytes(), plaintext_bytes); + + Ok(()) + } + + #[test] + fn test_lnurl_pay_validate_success_action_aes() -> Result<()> { + assert!(AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), + iv: base64::encode([0xa; 16]) + } + .validate() + .is_ok()); + + // Description longer than 144 chars + assert!(AesSuccessActionData { + description: rand_string(150), + ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), + iv: base64::encode([0xa; 16]) + } + .validate() + .is_err()); + + // IV size below 16 bytes (24 chars) + assert!(AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), + iv: base64::encode([0xa; 10]) + } + .validate() + .is_err()); + + // IV size above 16 bytes (24 chars) + assert!(AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), + iv: base64::encode([0xa; 20]) + } + .validate() + .is_err()); + + // IV is not base64 encoded (but fits length of 24 chars) + assert!(AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), + iv: ",".repeat(24) + } + .validate() + .is_err()); + + // Ciphertext is not base64 encoded + assert!(AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: ",".repeat(96), + iv: base64::encode([0xa; 16]) + } + .validate() + .is_err()); + + // Ciphertext longer than 4KB + assert!(AesSuccessActionData { + description: "Test AES successData description".into(), + ciphertext: base64::encode(rand_string(5000)), + iv: base64::encode([0xa; 16]) + } + .validate() + .is_err()); + + Ok(()) + } + + #[test] + fn test_lnurl_pay_validate_success_action_msg() -> Result<()> { + assert!(MessageSuccessActionData { + message: "short msg".into() + } + .validate() + .is_ok()); + + // Too long message + assert!(MessageSuccessActionData { + message: rand_string(150) + } + .validate() + .is_err()); + + Ok(()) + } + + #[test] + fn test_lnurl_pay_validate_success_url() -> Result<()> { + let pay_req_data = get_test_pay_req_data(0, 100_000, 100); + + assert!(UrlSuccessActionData { + description: "short msg".into(), + url: pay_req_data.callback.clone() + } + .validate(&pay_req_data) + .is_ok()); + + // Too long description + assert!(UrlSuccessActionData { + description: rand_string(150), + url: pay_req_data.callback.clone() + } + .validate(&pay_req_data) + .is_err()); + + // Different Success Action domain than in the callback URL + assert!(UrlSuccessActionData { + description: "short msg".into(), + url: "https://new-domain.com/test-url".into() + } + .validate(&pay_req_data) + .is_err()); + + Ok(()) + } +} diff --git a/libs/sdk-core/src/lnurl/withdraw.rs b/libs/sdk-common/src/lnurl/specs/withdraw.rs similarity index 58% rename from libs/sdk-core/src/lnurl/withdraw.rs rename to libs/sdk-common/src/lnurl/specs/withdraw.rs index 3353f707f..59df57a9a 100644 --- a/libs/sdk-core/src/lnurl/withdraw.rs +++ b/libs/sdk-common/src/lnurl/specs/withdraw.rs @@ -1,11 +1,6 @@ use std::str::FromStr; -use crate::input_parser::get_parse_and_log_response; -use crate::lnurl::error::LnUrlError; -use crate::{ - ensure_sdk, lnurl::*, LnUrlCallbackStatus, LnUrlWithdrawResult, LnUrlWithdrawSuccessData, -}; -use crate::{LNInvoice, LnUrlWithdrawRequestData}; +use crate::prelude::*; /// Validates invoice and performs the second and last step of LNURL-withdraw, as per /// @@ -15,7 +10,7 @@ use crate::{LNInvoice, LnUrlWithdrawRequestData}; /// 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(crate) async fn validate_lnurl_withdraw( +pub async fn validate_lnurl_withdraw( req_data: LnUrlWithdrawRequestData, invoice: LNInvoice, ) -> LnUrlResult { @@ -51,7 +46,7 @@ pub(crate) async fn validate_lnurl_withdraw( Ok(withdraw_status) } -fn build_withdraw_callback_url( +pub fn build_withdraw_callback_url( req_data: &LnUrlWithdrawRequestData, invoice: &LNInvoice, ) -> LnUrlResult { @@ -61,20 +56,135 @@ fn build_withdraw_callback_url( url.query_pairs_mut().append_pair("k1", &req_data.k1); url.query_pairs_mut().append_pair("pr", &invoice.bolt11); - let mut callback_url = url.to_string(); - callback_url = maybe_replace_host_with_mockito_test_host(callback_url)?; - Ok(callback_url) + Ok(url.to_string()) +} + +pub mod model { + use serde::{Deserialize, Serialize}; + use thiserror::Error; + + use crate::prelude::*; + + #[derive(Debug, Serialize, Deserialize)] + pub struct LnUrlWithdrawRequest { + /// Request data containing information on how to call the lnurl withdraw + /// endpoint. Typically retrieved by calling `parse()` on a lnurl withdraw + /// input. + pub data: LnUrlWithdrawRequestData, + + /// The amount to withdraw from the lnurl withdraw endpoint. Must be between + /// `min_withdrawable` and `max_withdrawable`. + pub amount_msat: u64, + + /// Optional description that will be put in the payment request for the + /// lnurl withdraw endpoint. + pub description: Option, + } + + /// Wrapped in a [LnUrlWithdraw], this is the result of [parse] when given a LNURL-withdraw endpoint. + /// + /// It represents the endpoint's parameters for the LNURL workflow. + /// + /// See + #[derive(Clone, Deserialize, Debug, Serialize)] + #[serde(rename_all = "camelCase")] + pub struct LnUrlWithdrawRequestData { + pub callback: String, + pub k1: String, + pub default_description: String, + /// The minimum amount, in millisats, that this LNURL-withdraw endpoint accepts + pub min_withdrawable: u64, + /// The maximum amount, in millisats, that this LNURL-withdraw endpoint accepts + pub max_withdrawable: u64, + } + + /// [LnUrlCallbackStatus] specific to LNURL-withdraw, where the success case contains the invoice. + #[derive(Clone, Serialize)] + pub enum LnUrlWithdrawResult { + Ok { data: LnUrlWithdrawSuccessData }, + ErrorStatus { data: LnUrlErrorData }, + } + + #[derive(Clone, Deserialize, Debug, Serialize)] + pub struct LnUrlWithdrawSuccessData { + pub invoice: LNInvoice, + } + + #[derive(Debug, Error)] + pub enum LnUrlWithdrawError { + /// This error is raised when a general error occurs not specific to other error variants + /// in this enum. + #[error("Generic: {err}")] + Generic { err: String }, + + /// This error is raised when the amount is zero or the amount does not cover + /// the cost to open a new channel. + #[error("Invalid amount: {err}")] + InvalidAmount { err: String }, + + /// This error is raised when the lightning invoice cannot be parsed. + #[error("Invalid invoice: {err}")] + InvalidInvoice { err: String }, + + /// This error is raised when the decoded LNURL URI is not compliant to the specification. + #[error("Invalid uri: {err}")] + InvalidUri { err: String }, + + /// This error is raised when no routing hints were able to be added to the invoice + /// while trying to receive a payment. + #[error("No routing hints: {err}")] + InvoiceNoRoutingHints { err: String }, + + /// This error is raised when a connection to an external service fails. + #[error("Service connectivity: {err}")] + ServiceConnectivity { err: String }, + } + + impl From for LnUrlWithdrawError { + fn from(value: InvoiceError) -> Self { + match value { + InvoiceError::Validation(err) => Self::InvalidInvoice { err }, + _ => Self::Generic { + err: value.to_string(), + }, + } + } + } + + impl From for LnUrlWithdrawError { + fn from(value: LnUrlError) -> Self { + match value { + LnUrlError::Generic(err) => Self::Generic { err }, + LnUrlError::InvalidUri(err) => Self::InvalidUri { err }, + LnUrlError::InvalidInvoice(err) => Self::InvalidInvoice { err }, + LnUrlError::ServiceConnectivity(err) => Self::ServiceConnectivity { err }, + } + } + } } #[cfg(test)] mod tests { use anyhow::Result; + use mockito::Mock; use crate::input_parser::tests::MOCK_HTTP_SERVER; - use crate::input_parser::LnUrlWithdrawRequestData; - use crate::lnurl::withdraw::*; - use crate::test_utils::rand_string; - use mockito::Mock; + use crate::lnurl::tests::rand_string; + use crate::prelude::*; + + #[tokio::test] + async fn test_lnurl_withdraw_validate_amount_failure() -> Result<()> { + 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()); + + Ok(()) + } /// Mock an LNURL-withdraw endpoint that responds with an OK to a withdraw attempt fn mock_lnurl_withdraw_callback( @@ -111,7 +221,7 @@ mod tests { max_withdrawable: max_sat * 1000, k1: rand_string(10), default_description: "test description".into(), - callback: "https://localhost/callback".into(), + callback: "http://127.0.0.1:8080/callback".into(), } } @@ -131,20 +241,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_lnurl_withdraw_validate_amount_failure() -> Result<()> { - 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()); - - Ok(()) - } - #[tokio::test] async fn test_lnurl_withdraw_endpoint_failure() -> Result<()> { let invoice_str = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; diff --git a/libs/sdk-common/src/model.rs b/libs/sdk-common/src/model.rs new file mode 100644 index 000000000..c5a9de542 --- /dev/null +++ b/libs/sdk-common/src/model.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::Display; + +/// The different supported bitcoin networks +#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, Serialize, Deserialize)] +pub enum Network { + /// Mainnet + Bitcoin, + Testnet, + Signet, + Regtest, +} + +impl From for Network { + fn from(network: bitcoin::network::constants::Network) -> Self { + match network { + bitcoin::network::constants::Network::Bitcoin => Network::Bitcoin, + bitcoin::network::constants::Network::Testnet => Network::Testnet, + bitcoin::network::constants::Network::Signet => Network::Signet, + bitcoin::network::constants::Network::Regtest => Network::Regtest, + } + } +} + +impl From for bitcoin::network::constants::Network { + fn from(network: Network) -> Self { + match network { + Network::Bitcoin => bitcoin::network::constants::Network::Bitcoin, + Network::Testnet => bitcoin::network::constants::Network::Testnet, + Network::Signet => bitcoin::network::constants::Network::Signet, + Network::Regtest => bitcoin::network::constants::Network::Regtest, + } + } +} diff --git a/libs/sdk-common/src/utils/mod.rs b/libs/sdk-common/src/utils/mod.rs new file mode 100644 index 000000000..dfc203baf --- /dev/null +++ b/libs/sdk-common/src/utils/mod.rs @@ -0,0 +1,10 @@ +pub(crate) mod rest_client; + +#[macro_export] +macro_rules! ensure_sdk { + ($cond:expr, $err:expr) => { + if !$cond { + return Err($err); + } + }; +} diff --git a/libs/sdk-common/src/utils/rest_client.rs b/libs/sdk-common/src/utils/rest_client.rs new file mode 100644 index 000000000..0de38bdd3 --- /dev/null +++ b/libs/sdk-common/src/utils/rest_client.rs @@ -0,0 +1,98 @@ +use std::time::Duration; + +use log::*; +use reqwest::StatusCode; +use thiserror::Error; + +#[derive(Debug, Error)] +#[error("{err}")] +pub struct ServiceConnectivityError { + pub err: String, +} +impl ServiceConnectivityError { + pub fn new(err: &str) -> Self { + ServiceConnectivityError { + err: err.to_string(), + } + } +} + +/// Creates an HTTP client with a built-in connection timeout +pub fn get_reqwest_client() -> Result { + reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .map_err(|e| ServiceConnectivityError { err: e.to_string() }) +} + +pub async fn post_and_log_response( + url: &str, + body: Option, +) -> Result { + debug!("Making POST request to: {url}"); + + let mut req = get_reqwest_client()?.post(url); + if let Some(body) = body { + req = req.body(body); + } + let raw_body = req + .send() + .await + .map_err(|e| ServiceConnectivityError::new(&e.to_string()))? + .text() + .await + .map_err(|e| ServiceConnectivityError::new(&e.to_string()))?; + debug!("Received 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}"); + + let response = get_reqwest_client()? + .get(url) + .send() + .await + .map_err(|e| ServiceConnectivityError::new(&e.to_string()))?; + let status = response.status(); + let raw_body = response + .text() + .await + .map_err(|e| ServiceConnectivityError::new(&e.to_string()))?; + debug!("Received response, status: {status}, raw response body: {raw_body}"); + + Ok((raw_body, status)) +} + +/// 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 +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() { + let err = format!("GET request {url} failed with status: {status}"); + error!("{err}"); + return Err(ServiceConnectivityError::new(&err)); + } + + serde_json::from_str::(&raw_body).map_err(|e| ServiceConnectivityError::new(&e.to_string())) +} diff --git a/libs/sdk-core/Cargo.toml b/libs/sdk-core/Cargo.toml index b803dcd37..c3a8dceee 100644 --- a/libs/sdk-core/Cargo.toml +++ b/libs/sdk-core/Cargo.toml @@ -9,18 +9,16 @@ crate-type = ["staticlib", "cdylib", "lib"] [dependencies] flutter_rust_bridge = "=1.82.6" -aes = "0.8" -anyhow = { version = "1.0.79", features = ["backtrace"] } -cbc = { version = "0.1", features = ["std"] } -hex = "0.4" -bip21 = "0.2" +aes = { workspace = true } +anyhow = { workspace = true } +hex = { workspace = true } # The last commit on gl-client 0.1. Development will continue on 0.2. # The switch to 0.2 will happen with https://github.com/breez/breez-sdk/pull/724 gl-client = { git = "https://github.com/Blockstream/greenlight.git", features = [ "permissive", ], rev = "e38a37613da7558c853f24be700c193f194a6bc9" } zbase32 = "0.1.2" -base64 = "0.13.0" +base64 = { workspace = true } chrono = "0.4" ecies = { version = "0.2.6", default-features = false, features = ["pure"] } env_logger = "0.10" @@ -28,21 +26,14 @@ futures = "0.3.30" ripemd = "0.1" rand = "0.8" tiny-bip39 = "1" -tokio = { version = "1", features = ["full"] } +tokio = { workspace = true } prost = "^0.11" -querystring = "1" -rusqlite = { version = "0.29", features = [ - "serde_json", - "bundled", - "backup", - "trace", - "hooks", -] } +rusqlite = { workspace = true } rusqlite_migration = "1.0" -# Pin the reqwest dependency until macOS linker issue is fixed: https://github.com/seanmonstar/reqwest/issues/2006 -reqwest = { version = "=0.11.20", features = ["json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sdk-common = { path = "../sdk-common" } tonic = { version = "^0.8", features = [ "tls", "transport", @@ -50,22 +41,21 @@ tonic = { version = "^0.8", features = [ "tls-webpki-roots", ] } lazy_static = "^1.4.0" -log = "0.4" -once_cell = "1" +log = { workspace = true } +once_cell = { workspace = true } openssl = { version = "0.10", features = ["vendored"] } -strum = "0.25" -strum_macros = "0.25" +strum = { workspace = true } +strum_macros = { workspace = true } tempfile = "3" -thiserror = "1.0.56" +thiserror = { workspace = true } const_format = "0.2" miniz_oxide = "0.7.1" tokio-stream = "0.1.14" serde_with = "3.3.0" -regex = "1.8.1" +regex = { workspace = true } [dev-dependencies] -mockito = "1" -regex = "1.8.1" +mockito = { workspace = true } [build-dependencies] tonic-build = "^0.8" diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs index 140bce792..64414d6a7 100644 --- a/libs/sdk-core/src/binding.rs +++ b/libs/sdk-core/src/binding.rs @@ -17,24 +17,29 @@ use anyhow::{anyhow, Result}; use flutter_rust_bridge::StreamSink; use log::{Level, LevelFilter, Metadata, Record}; use once_cell::sync::{Lazy, OnceCell}; +use sdk_common::invoice; +pub use sdk_common::prelude::{ + parse, AesSuccessActionDataDecrypted, AesSuccessActionDataResult, BitcoinAddressData, + InputType, LNInvoice, LnUrlAuthRequestData, LnUrlCallbackStatus, LnUrlError, LnUrlErrorData, + LnUrlPayErrorData, LnUrlPayRequest, LnUrlPayRequestData, LnUrlWithdrawRequest, + LnUrlWithdrawRequestData, LnUrlWithdrawResult, LnUrlWithdrawSuccessData, + MessageSuccessActionData, Network, RouteHint, RouteHintHop, SuccessActionProcessed, + UrlSuccessActionData, +}; use tokio::sync::Mutex; use crate::breez_services::{self, BreezEvent, BreezServices, EventListener}; use crate::chain::RecommendedFees; use crate::error::{ - ConnectError, LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError, ReceiveOnchainError, - ReceivePaymentError, SdkError, SendOnchainError, SendPaymentError, + ConnectError, LnUrlAuthError, ReceiveOnchainError, ReceivePaymentError, SdkError, + SendOnchainError, SendPaymentError, }; use crate::fiat::{FiatCurrency, Rate}; -use crate::input_parser::{self, InputType, LnUrlAuthRequestData}; -use crate::invoice::{self, LNInvoice}; -use crate::lnurl::pay::model::LnUrlPayResult; use crate::lsp::LspInformation; use crate::models::{Config, LogEntry, NodeState, Payment, SwapInfo}; use crate::{ BackupStatus, BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse, ConfigureNodeRequest, ConnectRequest, EnvironmentType, ListPaymentsRequest, - LnUrlCallbackStatus, LnUrlPayRequest, LnUrlWithdrawRequest, LnUrlWithdrawResult, MaxReverseSwapAmountResponse, NodeConfig, NodeCredentials, OnchainPaymentLimitsResponse, OpenChannelFeeRequest, OpenChannelFeeResponse, PayOnchainRequest, PayOnchainResponse, PrepareOnchainPaymentRequest, PrepareOnchainPaymentResponse, PrepareRedeemOnchainFundsRequest, @@ -47,6 +52,195 @@ use crate::{ SignMessageResponse, StaticBackupRequest, StaticBackupResponse, }; +// === FRB mirroring +// +// This section contains frb "mirroring" structs and enums. +// These are needed by the flutter bridge in order to use structs defined in an external crate. +// See +// Note: in addition to the docs above, the mirrored structs must derive the Clone trait + +use flutter_rust_bridge::frb; + +#[frb(mirror(LnUrlAuthRequestData))] +pub struct _LnUrlAuthRequestData { + pub k1: String, + pub action: Option, + pub domain: String, + pub url: String, +} + +#[frb(mirror(LnUrlErrorData))] +pub struct _LnUrlErrorData { + pub reason: String, +} + +#[frb(mirror(LnUrlCallbackStatus))] +pub enum _LnUrlCallbackStatus { + Ok, + ErrorStatus { data: LnUrlErrorData }, +} + +#[frb(mirror(Network))] +pub enum _Network { + Bitcoin, + Testnet, + Signet, + Regtest, +} + +#[frb(mirror(LNInvoice))] +pub struct _LNInvoice { + pub bolt11: String, + pub network: Network, + pub payee_pubkey: String, + pub payment_hash: String, + pub description: Option, + pub description_hash: Option, + pub amount_msat: Option, + pub timestamp: u64, + pub expiry: u64, + pub routing_hints: Vec, + pub payment_secret: Vec, + pub min_final_cltv_expiry_delta: u64, +} + +#[frb(mirror(RouteHint))] +pub struct _RouteHint { + pub hops: Vec, +} + +#[frb(mirror(RouteHintHop))] +pub struct _RouteHintHop { + pub src_node_id: String, + pub short_channel_id: u64, + pub fees_base_msat: u32, + pub fees_proportional_millionths: u32, + pub cltv_expiry_delta: u64, + pub htlc_minimum_msat: Option, + pub htlc_maximum_msat: Option, +} + +#[frb(mirror(LnUrlPayRequest))] +pub struct _LnUrlPayRequest { + pub data: LnUrlPayRequestData, + pub amount_msat: u64, + pub comment: Option, + pub payment_label: Option, +} + +#[frb(mirror(LnUrlPayRequestData))] +pub struct _LnUrlPayRequestData { + pub callback: String, + pub min_sendable: u64, + pub max_sendable: u64, + pub metadata_str: String, + pub comment_allowed: u16, + pub domain: String, + pub allows_nostr: bool, + pub nostr_pubkey: Option, + pub ln_address: Option, +} + +#[frb(mirror(LnUrlWithdrawRequest))] +pub struct _LnUrlWithdrawRequest { + pub data: LnUrlWithdrawRequestData, + pub amount_msat: u64, + pub description: Option, +} + +#[frb(mirror(LnUrlWithdrawRequestData))] +pub struct _LnUrlWithdrawRequestData { + pub callback: String, + pub k1: String, + pub default_description: String, + pub min_withdrawable: u64, + pub max_withdrawable: u64, +} + +#[frb(mirror(InputType))] +pub enum _InputType { + BitcoinAddress { address: BitcoinAddressData }, + Bolt11 { invoice: LNInvoice }, + NodeId { node_id: String }, + Url { url: String }, + LnUrlPay { data: LnUrlPayRequestData }, + LnUrlWithdraw { data: LnUrlWithdrawRequestData }, + LnUrlAuth { data: LnUrlAuthRequestData }, + LnUrlError { data: LnUrlErrorData }, +} + +#[frb(mirror(BitcoinAddressData))] +pub struct _BitcoinAddressData { + pub address: String, + pub network: crate::prelude::Network, + pub amount_sat: Option, + pub label: Option, + pub message: Option, +} + +#[frb(mirror(SuccessActionProcessed))] +pub enum _SuccessActionProcessed { + Aes { result: AesSuccessActionDataResult }, + Message { data: MessageSuccessActionData }, + Url { data: UrlSuccessActionData }, +} + +#[frb(mirror(AesSuccessActionDataResult))] +pub enum _AesSuccessActionDataResult { + Decrypted { data: AesSuccessActionDataDecrypted }, + ErrorStatus { reason: String }, +} + +#[frb(mirror(AesSuccessActionDataDecrypted))] +pub struct _AesSuccessActionDataDecrypted { + pub description: String, + pub plaintext: String, +} + +#[frb(mirror(MessageSuccessActionData))] +pub struct _MessageSuccessActionData { + pub message: String, +} + +#[frb(mirror(UrlSuccessActionData))] +pub struct _UrlSuccessActionData { + pub description: String, + pub url: String, +} + +#[frb(mirror(LnUrlPayErrorData))] +pub struct _LnUrlPayErrorData { + pub payment_hash: String, + pub reason: String, +} + +#[frb(mirror(LnUrlPayError))] +pub enum _LnUrlPayError { + AlreadyPaid, + Generic { err: String }, + InvalidAmount { err: String }, + InvalidInvoice { err: String }, + InvalidNetwork { err: String }, + InvalidUri { err: String }, + InvoiceExpired { err: String }, + PaymentFailed { err: String }, + PaymentTimeout { err: String }, + RouteNotFound { err: String }, + RouteTooExpensive { err: String }, + ServiceConnectivity { err: String }, +} + +#[frb(mirror(LnUrlWithdrawResult))] +pub enum _LnUrlWithdrawResult { + Ok { data: LnUrlWithdrawSuccessData }, + ErrorStatus { data: LnUrlErrorData }, +} + +#[frb(mirror(LnUrlWithdrawSuccessData))] +pub struct _LnUrlWithdrawSuccessData { + pub invoice: LNInvoice, +} + /* The format Lazy>> for the following variables allows them to be instance-global, meaning they can be set only once per instance, but calling disconnect() will unset them. @@ -268,7 +462,7 @@ pub fn parse_invoice(invoice: String) -> Result { } pub fn parse_input(input: String) -> Result { - block_on(async { input_parser::parse(&input).await }) + block_on(async { parse(&input).await }) } /* Payment API's */ @@ -324,19 +518,19 @@ pub fn receive_payment(req: ReceivePaymentRequest) -> Result Result { +pub fn lnurl_pay(req: LnUrlPayRequest) -> Result { block_on(async { get_breez_services().await?.lnurl_pay(req).await }) - .map_err(anyhow::Error::new::) + .map_err(anyhow::Error::new::) } /// See [BreezServices::lnurl_withdraw] pub fn lnurl_withdraw(req: LnUrlWithdrawRequest) -> Result { block_on(async { get_breez_services().await?.lnurl_withdraw(req).await }) - .map_err(anyhow::Error::new::) + .map_err(anyhow::Error::new::) } /// See [BreezServices::lnurl_auth] -pub fn lnurl_auth(req_data: LnUrlAuthRequestData) -> Result { +pub fn lnurl_auth(req_data: crate::LnUrlAuthRequestData) -> Result { block_on(async { get_breez_services().await?.lnurl_auth(req_data).await }) .map_err(anyhow::Error::new::) } diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 35cdcd980..4fef1916f 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -12,8 +12,10 @@ use bitcoin::hashes::{sha256, Hash}; use bitcoin::util::bip32::ChildNumber; use chrono::Local; use futures::TryFutureExt; +use gl_client::bitcoin::secp256k1::Secp256k1; use log::{LevelFilter, Metadata, Record}; -use reqwest::{header::CONTENT_TYPE, Body}; +use reqwest::{header::CONTENT_TYPE, Body, Url}; +use sdk_common::prelude::*; use serde_json::json; use tokio::sync::{mpsc, watch, Mutex}; use tokio::time::{sleep, MissedTickBehavior}; @@ -30,8 +32,8 @@ use crate::chain::{ DEFAULT_MEMPOOL_SPACE_URL, }; use crate::error::{ - LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError, ReceiveOnchainError, ReceiveOnchainResult, - ReceivePaymentError, SdkError, SdkResult, SendOnchainError, SendPaymentError, + ConnectError, LnUrlAuthError, ReceiveOnchainError, ReceiveOnchainResult, ReceivePaymentError, + SdkError, SdkResult, SendOnchainError, SendPaymentError, }; use crate::fiat::{FiatCurrency, Rate}; use crate::greenlight::{GLBackupTransport, Greenlight}; @@ -42,23 +44,12 @@ use crate::grpc::signer_client::SignerClient; use crate::grpc::support_client::SupportClient; use crate::grpc::swapper_client::SwapperClient; use crate::grpc::{ChainApiServersRequest, PaymentInformation}; -use crate::input_parser::get_reqwest_client; -use crate::invoice::{ - add_routing_hints, parse_invoice, validate_network, LNInvoice, RouteHint, RouteHintHop, -}; -use crate::lnurl::auth::perform_lnurl_auth; -use crate::lnurl::pay::model::SuccessAction::Aes; -use crate::lnurl::pay::model::{ - LnUrlPayResult, SuccessAction, SuccessActionProcessed, ValidatedCallbackResponse, -}; -use crate::lnurl::pay::validate_lnurl_pay; -use crate::lnurl::withdraw::validate_lnurl_withdraw; +use crate::lnurl::pay::*; use crate::lsp::LspInformation; use crate::models::{ parse_short_channel_id, ChannelState, ClosedChannelPaymentDetails, Config, EnvironmentType, - FiatAPI, LnUrlCallbackStatus, LspAPI, NodeState, Payment, PaymentDetails, PaymentType, - ReverseSwapPairInfo, ReverseSwapServiceAPI, SwapInfo, SwapperAPI, - INVOICE_PAYMENT_FEE_EXPIRY_SECONDS, + FiatAPI, LspAPI, NodeState, Payment, PaymentDetails, PaymentType, ReverseSwapPairInfo, + ReverseSwapServiceAPI, SwapInfo, SwapperAPI, INVOICE_PAYMENT_FEE_EXPIRY_SECONDS, }; use crate::moonpay::MoonPayApi; use crate::node_api::{CreateInvoiceRequest, NodeAPI}; @@ -69,7 +60,6 @@ use crate::swap_out::reverseswap::{BTCSendSwap, CreateReverseSwapArg}; use crate::BuyBitcoinProvider::Moonpay; use crate::*; -use self::error::ConnectError; use self::grpc::PingRequest; pub type BreezServicesResult = Result; @@ -416,7 +406,7 @@ impl BreezServices { Some(sa) => { let processed_sa = match sa { // For AES, we decrypt the contents on the fly - Aes(data) => { + SuccessAction::Aes(data) => { let preimage = sha256::Hash::from_str(&details.payment_preimage)?; let preimage_arr: [u8; 32] = preimage.into_inner(); let result = match (data, &preimage_arr).try_into() { @@ -457,7 +447,7 @@ impl BreezServices { )?; Ok(LnUrlPayResult::EndpointSuccess { - data: LnUrlPaySuccessData { + data: lnurl::pay::LnUrlPaySuccessData { payment, success_action: maybe_sa_processed, }, @@ -518,7 +508,20 @@ impl BreezServices { &self, req_data: LnUrlAuthRequestData, ) -> Result { - Ok(perform_lnurl_auth(self.node_api.clone(), req_data).await?) + // m/138'/0 + let hashing_key = self.node_api.derive_bip32_key(vec![ + ChildNumber::from_hardened_idx(138).map_err(Into::::into)?, + ChildNumber::from(0), + ])?; + + let url = + Url::from_str(&req_data.url).map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; + + let derivation_path = get_derivation_path(hashing_key, url)?; + let linking_key = self.node_api.derive_bip32_key(derivation_path)?; + let linking_keys = linking_key.to_keypair(&Secp256k1::new()); + + Ok(perform_lnurl_auth(linking_keys, req_data).await?) } /// Creates an bolt11 payment request. @@ -2742,17 +2745,10 @@ pub(crate) mod tests { use crate::breez_services::{BreezServices, BreezServicesBuilder}; use crate::fiat::Rate; - use crate::lnurl::pay::model::MessageSuccessActionData; - use crate::lnurl::pay::model::SuccessActionProcessed; use crate::models::{LnPaymentDetails, NodeState, Payment, PaymentDetails, PaymentTypeFilter}; use crate::node_api::NodeAPI; - use crate::{ - input_parser, parse_short_channel_id, test_utils::*, BuyBitcoinProvider, BuyBitcoinRequest, - FullReverseSwapInfo, InputType, ListPaymentsRequest, OpeningFeeParams, PaymentStatus, - ReceivePaymentRequest, ReverseSwapInfo, ReverseSwapInfoCached, ReverseSwapStatus, SwapInfo, - SwapStatus, - }; - use crate::{PaymentExternalInfo, PaymentType}; + use crate::test_utils::*; + use crate::*; use super::{PaymentReceiver, Receiver}; @@ -3196,7 +3192,7 @@ pub(crate) mod tests { assert_eq!(parsed.host_str(), Some("mock.moonpay")); assert_eq!(parsed.path(), "/"); - let wallet_address = input_parser::parse(query_pairs.get("wa").unwrap()).await?; + let wallet_address = parse(query_pairs.get("wa").unwrap()).await?; assert!(matches!(wallet_address, InputType::BitcoinAddress { .. })); let max_amount = query_pairs.get("ma").unwrap(); diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index a4bc43b1f..caf7bbd56 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -35,23 +35,8 @@ use crate::fiat::LocaleOverrides; use crate::fiat::LocalizedName; use crate::fiat::Rate; use crate::fiat::Symbol; -use crate::input_parser::BitcoinAddressData; -use crate::input_parser::InputType; -use crate::input_parser::LnUrlAuthRequestData; -use crate::input_parser::LnUrlErrorData; -use crate::input_parser::LnUrlPayRequestData; -use crate::input_parser::LnUrlWithdrawRequestData; -use crate::invoice::LNInvoice; -use crate::invoice::RouteHint; -use crate::invoice::RouteHintHop; -use crate::lnurl::pay::model::AesSuccessActionDataDecrypted; -use crate::lnurl::pay::model::AesSuccessActionDataResult; -use crate::lnurl::pay::model::LnUrlPayErrorData; -use crate::lnurl::pay::model::LnUrlPayResult; -use crate::lnurl::pay::model::LnUrlPaySuccessData; -use crate::lnurl::pay::model::MessageSuccessActionData; -use crate::lnurl::pay::model::SuccessActionProcessed; -use crate::lnurl::pay::model::UrlSuccessActionData; +use crate::lnurl::pay::LnUrlPayResult; +use crate::lnurl::pay::LnUrlPaySuccessData; use crate::lsp::LspInformation; use crate::models::BackupStatus; use crate::models::BuyBitcoinProvider; @@ -69,15 +54,9 @@ use crate::models::GreenlightNodeConfig; use crate::models::HealthCheckStatus; use crate::models::ListPaymentsRequest; use crate::models::LnPaymentDetails; -use crate::models::LnUrlCallbackStatus; -use crate::models::LnUrlPayRequest; -use crate::models::LnUrlWithdrawRequest; -use crate::models::LnUrlWithdrawResult; -use crate::models::LnUrlWithdrawSuccessData; use crate::models::LogEntry; use crate::models::MaxReverseSwapAmountResponse; use crate::models::MetadataFilter; -use crate::models::Network; use crate::models::NodeConfig; use crate::models::NodeCredentials; use crate::models::NodeState; @@ -438,7 +417,7 @@ fn wire_backup_status_impl(port_: MessagePort) { ) } fn wire_parse_invoice_impl(port_: MessagePort, invoice: impl Wire2Api + UnwindSafe) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, LNInvoice, _>( + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, mirror_LNInvoice, _>( WrapInfo { debug_name: "parse_invoice", port: Some(port_), @@ -451,7 +430,7 @@ fn wire_parse_invoice_impl(port_: MessagePort, invoice: impl Wire2Api + ) } fn wire_parse_input_impl(port_: MessagePort, input: impl Wire2Api + UnwindSafe) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, InputType, _>( + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, mirror_InputType, _>( WrapInfo { debug_name: "parse_input", port: Some(port_), @@ -572,7 +551,7 @@ fn wire_lnurl_withdraw_impl( port_: MessagePort, req: impl Wire2Api + UnwindSafe, ) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, LnUrlWithdrawResult, _>( + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, mirror_LnUrlWithdrawResult, _>( WrapInfo { debug_name: "lnurl_withdraw", port: Some(port_), @@ -588,7 +567,7 @@ fn wire_lnurl_auth_impl( port_: MessagePort, req_data: impl Wire2Api + UnwindSafe, ) { - FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, LnUrlCallbackStatus, _>( + FLUTTER_RUST_BRIDGE_HANDLER.wrap::<_, _, _, mirror_LnUrlCallbackStatus, _>( WrapInfo { debug_name: "lnurl_auth", port: Some(port_), @@ -915,8 +894,223 @@ fn wire_generate_diagnostic_data_impl(port_: MessagePort) { } // Section: wrapper structs +#[derive(Clone)] +pub struct mirror_AesSuccessActionDataDecrypted(AesSuccessActionDataDecrypted); + +#[derive(Clone)] +pub struct mirror_AesSuccessActionDataResult(AesSuccessActionDataResult); + +#[derive(Clone)] +pub struct mirror_BitcoinAddressData(BitcoinAddressData); + +#[derive(Clone)] +pub struct mirror_InputType(InputType); + +#[derive(Clone)] +pub struct mirror_LNInvoice(LNInvoice); + +#[derive(Clone)] +pub struct mirror_LnUrlAuthRequestData(LnUrlAuthRequestData); + +#[derive(Clone)] +pub struct mirror_LnUrlCallbackStatus(LnUrlCallbackStatus); + +#[derive(Clone)] +pub struct mirror_LnUrlErrorData(LnUrlErrorData); + +#[derive(Clone)] +pub struct mirror_LnUrlPayErrorData(LnUrlPayErrorData); + +#[derive(Clone)] +pub struct mirror_LnUrlPayRequestData(LnUrlPayRequestData); + +#[derive(Clone)] +pub struct mirror_LnUrlWithdrawRequestData(LnUrlWithdrawRequestData); + +#[derive(Clone)] +pub struct mirror_LnUrlWithdrawResult(LnUrlWithdrawResult); + +#[derive(Clone)] +pub struct mirror_LnUrlWithdrawSuccessData(LnUrlWithdrawSuccessData); + +#[derive(Clone)] +pub struct mirror_MessageSuccessActionData(MessageSuccessActionData); + +#[derive(Clone)] +pub struct mirror_Network(Network); + +#[derive(Clone)] +pub struct mirror_RouteHint(RouteHint); + +#[derive(Clone)] +pub struct mirror_RouteHintHop(RouteHintHop); + +#[derive(Clone)] +pub struct mirror_SuccessActionProcessed(SuccessActionProcessed); + +#[derive(Clone)] +pub struct mirror_UrlSuccessActionData(UrlSuccessActionData); + // Section: static checks +const _: fn() = || { + { + let AesSuccessActionDataDecrypted = None::.unwrap(); + let _: String = AesSuccessActionDataDecrypted.description; + let _: String = AesSuccessActionDataDecrypted.plaintext; + } + match None::.unwrap() { + AesSuccessActionDataResult::Decrypted { data } => { + let _: AesSuccessActionDataDecrypted = data; + } + AesSuccessActionDataResult::ErrorStatus { reason } => { + let _: String = reason; + } + } + { + let BitcoinAddressData = None::.unwrap(); + let _: String = BitcoinAddressData.address; + let _: Network = BitcoinAddressData.network; + let _: Option = BitcoinAddressData.amount_sat; + let _: Option = BitcoinAddressData.label; + let _: Option = BitcoinAddressData.message; + } + match None::.unwrap() { + InputType::BitcoinAddress { address } => { + let _: BitcoinAddressData = address; + } + InputType::Bolt11 { invoice } => { + let _: LNInvoice = invoice; + } + InputType::NodeId { node_id } => { + let _: String = node_id; + } + InputType::Url { url } => { + let _: String = url; + } + InputType::LnUrlPay { data } => { + let _: LnUrlPayRequestData = data; + } + InputType::LnUrlWithdraw { data } => { + let _: LnUrlWithdrawRequestData = data; + } + InputType::LnUrlAuth { data } => { + let _: LnUrlAuthRequestData = data; + } + InputType::LnUrlError { data } => { + let _: LnUrlErrorData = data; + } + } + { + let LNInvoice = None::.unwrap(); + let _: String = LNInvoice.bolt11; + let _: Network = LNInvoice.network; + let _: String = LNInvoice.payee_pubkey; + let _: String = LNInvoice.payment_hash; + let _: Option = LNInvoice.description; + let _: Option = LNInvoice.description_hash; + let _: Option = LNInvoice.amount_msat; + let _: u64 = LNInvoice.timestamp; + let _: u64 = LNInvoice.expiry; + let _: Vec = LNInvoice.routing_hints; + let _: Vec = LNInvoice.payment_secret; + let _: u64 = LNInvoice.min_final_cltv_expiry_delta; + } + { + let LnUrlAuthRequestData = None::.unwrap(); + let _: String = LnUrlAuthRequestData.k1; + let _: Option = LnUrlAuthRequestData.action; + let _: String = LnUrlAuthRequestData.domain; + let _: String = LnUrlAuthRequestData.url; + } + match None::.unwrap() { + LnUrlCallbackStatus::Ok => {} + LnUrlCallbackStatus::ErrorStatus { data } => { + let _: LnUrlErrorData = data; + } + } + { + let LnUrlErrorData = None::.unwrap(); + let _: String = LnUrlErrorData.reason; + } + { + let LnUrlPayErrorData = None::.unwrap(); + let _: String = LnUrlPayErrorData.payment_hash; + let _: String = LnUrlPayErrorData.reason; + } + { + let LnUrlPayRequestData = None::.unwrap(); + let _: String = LnUrlPayRequestData.callback; + let _: u64 = LnUrlPayRequestData.min_sendable; + let _: u64 = LnUrlPayRequestData.max_sendable; + let _: String = LnUrlPayRequestData.metadata_str; + let _: u16 = LnUrlPayRequestData.comment_allowed; + let _: String = LnUrlPayRequestData.domain; + let _: bool = LnUrlPayRequestData.allows_nostr; + let _: Option = LnUrlPayRequestData.nostr_pubkey; + let _: Option = LnUrlPayRequestData.ln_address; + } + { + let LnUrlWithdrawRequestData = None::.unwrap(); + let _: String = LnUrlWithdrawRequestData.callback; + let _: String = LnUrlWithdrawRequestData.k1; + let _: String = LnUrlWithdrawRequestData.default_description; + let _: u64 = LnUrlWithdrawRequestData.min_withdrawable; + let _: u64 = LnUrlWithdrawRequestData.max_withdrawable; + } + match None::.unwrap() { + LnUrlWithdrawResult::Ok { data } => { + let _: LnUrlWithdrawSuccessData = data; + } + LnUrlWithdrawResult::ErrorStatus { data } => { + let _: LnUrlErrorData = data; + } + } + { + let LnUrlWithdrawSuccessData = None::.unwrap(); + let _: LNInvoice = LnUrlWithdrawSuccessData.invoice; + } + { + let MessageSuccessActionData = None::.unwrap(); + let _: String = MessageSuccessActionData.message; + } + match None::.unwrap() { + Network::Bitcoin => {} + Network::Testnet => {} + Network::Signet => {} + Network::Regtest => {} + } + { + let RouteHint = None::.unwrap(); + let _: Vec = RouteHint.hops; + } + { + let RouteHintHop = None::.unwrap(); + let _: String = RouteHintHop.src_node_id; + let _: u64 = RouteHintHop.short_channel_id; + let _: u32 = RouteHintHop.fees_base_msat; + let _: u32 = RouteHintHop.fees_proportional_millionths; + let _: u64 = RouteHintHop.cltv_expiry_delta; + let _: Option = RouteHintHop.htlc_minimum_msat; + let _: Option = RouteHintHop.htlc_maximum_msat; + } + match None::.unwrap() { + SuccessActionProcessed::Aes { result } => { + let _: AesSuccessActionDataResult = result; + } + SuccessActionProcessed::Message { data } => { + let _: MessageSuccessActionData = data; + } + SuccessActionProcessed::Url { data } => { + let _: UrlSuccessActionData = data; + } + } + { + let UrlSuccessActionData = None::.unwrap(); + let _: String = UrlSuccessActionData.description; + let _: String = UrlSuccessActionData.url; + } +}; // Section: allocate functions // Section: related functions @@ -1033,37 +1227,41 @@ impl Wire2Api for u8 { // Section: impl IntoDart -impl support::IntoDart for AesSuccessActionDataDecrypted { +impl support::IntoDart for mirror_AesSuccessActionDataDecrypted { fn into_dart(self) -> support::DartAbi { vec![ - self.description.into_into_dart().into_dart(), - self.plaintext.into_into_dart().into_dart(), + self.0.description.into_into_dart().into_dart(), + self.0.plaintext.into_into_dart().into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for AesSuccessActionDataDecrypted {} -impl rust2dart::IntoIntoDart for AesSuccessActionDataDecrypted { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_AesSuccessActionDataDecrypted {} +impl rust2dart::IntoIntoDart + for AesSuccessActionDataDecrypted +{ + fn into_into_dart(self) -> mirror_AesSuccessActionDataDecrypted { + mirror_AesSuccessActionDataDecrypted(self) } } -impl support::IntoDart for AesSuccessActionDataResult { +impl support::IntoDart for mirror_AesSuccessActionDataResult { fn into_dart(self) -> support::DartAbi { - match self { - Self::Decrypted { data } => vec![0.into_dart(), data.into_into_dart().into_dart()], - Self::ErrorStatus { reason } => { + match self.0 { + AesSuccessActionDataResult::Decrypted { data } => { + vec![0.into_dart(), data.into_into_dart().into_dart()] + } + AesSuccessActionDataResult::ErrorStatus { reason } => { vec![1.into_dart(), reason.into_into_dart().into_dart()] } } .into_dart() } } -impl support::IntoDartExceptPrimitive for AesSuccessActionDataResult {} -impl rust2dart::IntoIntoDart for AesSuccessActionDataResult { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_AesSuccessActionDataResult {} +impl rust2dart::IntoIntoDart for AesSuccessActionDataResult { + fn into_into_dart(self) -> mirror_AesSuccessActionDataResult { + mirror_AesSuccessActionDataResult(self) } } @@ -1095,22 +1293,22 @@ impl rust2dart::IntoIntoDart for BackupStatus { } } -impl support::IntoDart for BitcoinAddressData { +impl support::IntoDart for mirror_BitcoinAddressData { fn into_dart(self) -> support::DartAbi { vec![ - self.address.into_into_dart().into_dart(), - self.network.into_into_dart().into_dart(), - self.amount_sat.into_dart(), - self.label.into_dart(), - self.message.into_dart(), + self.0.address.into_into_dart().into_dart(), + self.0.network.into_into_dart().into_dart(), + self.0.amount_sat.into_dart(), + self.0.label.into_dart(), + self.0.message.into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for BitcoinAddressData {} -impl rust2dart::IntoIntoDart for BitcoinAddressData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_BitcoinAddressData {} +impl rust2dart::IntoIntoDart for BitcoinAddressData { + fn into_into_dart(self) -> mirror_BitcoinAddressData { + mirror_BitcoinAddressData(self) } } @@ -1334,27 +1532,35 @@ impl rust2dart::IntoIntoDart for HealthCheckStatus { } } -impl support::IntoDart for InputType { +impl support::IntoDart for mirror_InputType { fn into_dart(self) -> support::DartAbi { - match self { - Self::BitcoinAddress { address } => { + match self.0 { + InputType::BitcoinAddress { address } => { vec![0.into_dart(), address.into_into_dart().into_dart()] } - Self::Bolt11 { invoice } => vec![1.into_dart(), invoice.into_into_dart().into_dart()], - Self::NodeId { node_id } => vec![2.into_dart(), node_id.into_into_dart().into_dart()], - Self::Url { url } => vec![3.into_dart(), url.into_into_dart().into_dart()], - Self::LnUrlPay { data } => vec![4.into_dart(), data.into_into_dart().into_dart()], - Self::LnUrlWithdraw { data } => vec![5.into_dart(), data.into_into_dart().into_dart()], - Self::LnUrlAuth { data } => vec![6.into_dart(), data.into_into_dart().into_dart()], - Self::LnUrlError { data } => vec![7.into_dart(), data.into_into_dart().into_dart()], + InputType::Bolt11 { invoice } => { + vec![1.into_dart(), invoice.into_into_dart().into_dart()] + } + InputType::NodeId { node_id } => { + vec![2.into_dart(), node_id.into_into_dart().into_dart()] + } + InputType::Url { url } => vec![3.into_dart(), url.into_into_dart().into_dart()], + InputType::LnUrlPay { data } => vec![4.into_dart(), data.into_into_dart().into_dart()], + InputType::LnUrlWithdraw { data } => { + vec![5.into_dart(), data.into_into_dart().into_dart()] + } + InputType::LnUrlAuth { data } => vec![6.into_dart(), data.into_into_dart().into_dart()], + InputType::LnUrlError { data } => { + vec![7.into_dart(), data.into_into_dart().into_dart()] + } } .into_dart() } } -impl support::IntoDartExceptPrimitive for InputType {} -impl rust2dart::IntoIntoDart for InputType { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_InputType {} +impl rust2dart::IntoIntoDart for InputType { + fn into_into_dart(self) -> mirror_InputType { + mirror_InputType(self) } } @@ -1375,31 +1581,32 @@ impl rust2dart::IntoIntoDart for InvoicePaidDetails { } } -impl support::IntoDart for LNInvoice { +impl support::IntoDart for mirror_LNInvoice { fn into_dart(self) -> support::DartAbi { vec![ - self.bolt11.into_into_dart().into_dart(), - self.network.into_into_dart().into_dart(), - self.payee_pubkey.into_into_dart().into_dart(), - self.payment_hash.into_into_dart().into_dart(), - self.description.into_dart(), - self.description_hash.into_dart(), - self.amount_msat.into_dart(), - self.timestamp.into_into_dart().into_dart(), - self.expiry.into_into_dart().into_dart(), - self.routing_hints.into_into_dart().into_dart(), - self.payment_secret.into_into_dart().into_dart(), - self.min_final_cltv_expiry_delta + self.0.bolt11.into_into_dart().into_dart(), + self.0.network.into_into_dart().into_dart(), + self.0.payee_pubkey.into_into_dart().into_dart(), + self.0.payment_hash.into_into_dart().into_dart(), + self.0.description.into_dart(), + self.0.description_hash.into_dart(), + self.0.amount_msat.into_dart(), + self.0.timestamp.into_into_dart().into_dart(), + self.0.expiry.into_into_dart().into_dart(), + self.0.routing_hints.into_into_dart().into_dart(), + self.0.payment_secret.into_into_dart().into_dart(), + self.0 + .min_final_cltv_expiry_delta .into_into_dart() .into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for LNInvoice {} -impl rust2dart::IntoIntoDart for LNInvoice { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LNInvoice {} +impl rust2dart::IntoIntoDart for LNInvoice { + fn into_into_dart(self) -> mirror_LNInvoice { + mirror_LNInvoice(self) } } @@ -1413,7 +1620,9 @@ impl support::IntoDart for LnPaymentDetails { self.keysend.into_into_dart().into_dart(), self.bolt11.into_into_dart().into_dart(), self.open_channel_bolt11.into_dart(), - self.lnurl_success_action.into_dart(), + self.lnurl_success_action + .map(|v| mirror_SuccessActionProcessed(v)) + .into_dart(), self.lnurl_pay_domain.into_dart(), self.lnurl_pay_comment.into_dart(), self.ln_address.into_dart(), @@ -1433,88 +1642,90 @@ impl rust2dart::IntoIntoDart for LnPaymentDetails { } } -impl support::IntoDart for LnUrlAuthRequestData { +impl support::IntoDart for mirror_LnUrlAuthRequestData { fn into_dart(self) -> support::DartAbi { vec![ - self.k1.into_into_dart().into_dart(), - self.action.into_dart(), - self.domain.into_into_dart().into_dart(), - self.url.into_into_dart().into_dart(), + self.0.k1.into_into_dart().into_dart(), + self.0.action.into_dart(), + self.0.domain.into_into_dart().into_dart(), + self.0.url.into_into_dart().into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlAuthRequestData {} -impl rust2dart::IntoIntoDart for LnUrlAuthRequestData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlAuthRequestData {} +impl rust2dart::IntoIntoDart for LnUrlAuthRequestData { + fn into_into_dart(self) -> mirror_LnUrlAuthRequestData { + mirror_LnUrlAuthRequestData(self) } } -impl support::IntoDart for LnUrlCallbackStatus { +impl support::IntoDart for mirror_LnUrlCallbackStatus { fn into_dart(self) -> support::DartAbi { - match self { - Self::Ok => vec![0.into_dart()], - Self::ErrorStatus { data } => vec![1.into_dart(), data.into_into_dart().into_dart()], + match self.0 { + LnUrlCallbackStatus::Ok => vec![0.into_dart()], + LnUrlCallbackStatus::ErrorStatus { data } => { + vec![1.into_dart(), data.into_into_dart().into_dart()] + } } .into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlCallbackStatus {} -impl rust2dart::IntoIntoDart for LnUrlCallbackStatus { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlCallbackStatus {} +impl rust2dart::IntoIntoDart for LnUrlCallbackStatus { + fn into_into_dart(self) -> mirror_LnUrlCallbackStatus { + mirror_LnUrlCallbackStatus(self) } } -impl support::IntoDart for LnUrlErrorData { +impl support::IntoDart for mirror_LnUrlErrorData { fn into_dart(self) -> support::DartAbi { - vec![self.reason.into_into_dart().into_dart()].into_dart() + vec![self.0.reason.into_into_dart().into_dart()].into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlErrorData {} -impl rust2dart::IntoIntoDart for LnUrlErrorData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlErrorData {} +impl rust2dart::IntoIntoDart for LnUrlErrorData { + fn into_into_dart(self) -> mirror_LnUrlErrorData { + mirror_LnUrlErrorData(self) } } -impl support::IntoDart for LnUrlPayErrorData { +impl support::IntoDart for mirror_LnUrlPayErrorData { fn into_dart(self) -> support::DartAbi { vec![ - self.payment_hash.into_into_dart().into_dart(), - self.reason.into_into_dart().into_dart(), + self.0.payment_hash.into_into_dart().into_dart(), + self.0.reason.into_into_dart().into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlPayErrorData {} -impl rust2dart::IntoIntoDart for LnUrlPayErrorData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlPayErrorData {} +impl rust2dart::IntoIntoDart for LnUrlPayErrorData { + fn into_into_dart(self) -> mirror_LnUrlPayErrorData { + mirror_LnUrlPayErrorData(self) } } -impl support::IntoDart for LnUrlPayRequestData { +impl support::IntoDart for mirror_LnUrlPayRequestData { fn into_dart(self) -> support::DartAbi { vec![ - self.callback.into_into_dart().into_dart(), - self.min_sendable.into_into_dart().into_dart(), - self.max_sendable.into_into_dart().into_dart(), - self.metadata_str.into_into_dart().into_dart(), - self.comment_allowed.into_into_dart().into_dart(), - self.domain.into_into_dart().into_dart(), - self.allows_nostr.into_into_dart().into_dart(), - self.nostr_pubkey.into_dart(), - self.ln_address.into_dart(), + self.0.callback.into_into_dart().into_dart(), + self.0.min_sendable.into_into_dart().into_dart(), + self.0.max_sendable.into_into_dart().into_dart(), + self.0.metadata_str.into_into_dart().into_dart(), + self.0.comment_allowed.into_into_dart().into_dart(), + self.0.domain.into_into_dart().into_dart(), + self.0.allows_nostr.into_into_dart().into_dart(), + self.0.nostr_pubkey.into_dart(), + self.0.ln_address.into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlPayRequestData {} -impl rust2dart::IntoIntoDart for LnUrlPayRequestData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlPayRequestData {} +impl rust2dart::IntoIntoDart for LnUrlPayRequestData { + fn into_into_dart(self) -> mirror_LnUrlPayRequestData { + mirror_LnUrlPayRequestData(self) } } @@ -1541,7 +1752,9 @@ impl support::IntoDart for LnUrlPaySuccessData { fn into_dart(self) -> support::DartAbi { vec![ self.payment.into_into_dart().into_dart(), - self.success_action.into_dart(), + self.success_action + .map(|v| mirror_SuccessActionProcessed(v)) + .into_dart(), ] .into_dart() } @@ -1553,50 +1766,54 @@ impl rust2dart::IntoIntoDart for LnUrlPaySuccessData { } } -impl support::IntoDart for LnUrlWithdrawRequestData { +impl support::IntoDart for mirror_LnUrlWithdrawRequestData { fn into_dart(self) -> support::DartAbi { vec![ - self.callback.into_into_dart().into_dart(), - self.k1.into_into_dart().into_dart(), - self.default_description.into_into_dart().into_dart(), - self.min_withdrawable.into_into_dart().into_dart(), - self.max_withdrawable.into_into_dart().into_dart(), + self.0.callback.into_into_dart().into_dart(), + self.0.k1.into_into_dart().into_dart(), + self.0.default_description.into_into_dart().into_dart(), + self.0.min_withdrawable.into_into_dart().into_dart(), + self.0.max_withdrawable.into_into_dart().into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlWithdrawRequestData {} -impl rust2dart::IntoIntoDart for LnUrlWithdrawRequestData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlWithdrawRequestData {} +impl rust2dart::IntoIntoDart for LnUrlWithdrawRequestData { + fn into_into_dart(self) -> mirror_LnUrlWithdrawRequestData { + mirror_LnUrlWithdrawRequestData(self) } } -impl support::IntoDart for LnUrlWithdrawResult { +impl support::IntoDart for mirror_LnUrlWithdrawResult { fn into_dart(self) -> support::DartAbi { - match self { - Self::Ok { data } => vec![0.into_dart(), data.into_into_dart().into_dart()], - Self::ErrorStatus { data } => vec![1.into_dart(), data.into_into_dart().into_dart()], + match self.0 { + LnUrlWithdrawResult::Ok { data } => { + vec![0.into_dart(), data.into_into_dart().into_dart()] + } + LnUrlWithdrawResult::ErrorStatus { data } => { + vec![1.into_dart(), data.into_into_dart().into_dart()] + } } .into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlWithdrawResult {} -impl rust2dart::IntoIntoDart for LnUrlWithdrawResult { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlWithdrawResult {} +impl rust2dart::IntoIntoDart for LnUrlWithdrawResult { + fn into_into_dart(self) -> mirror_LnUrlWithdrawResult { + mirror_LnUrlWithdrawResult(self) } } -impl support::IntoDart for LnUrlWithdrawSuccessData { +impl support::IntoDart for mirror_LnUrlWithdrawSuccessData { fn into_dart(self) -> support::DartAbi { - vec![self.invoice.into_into_dart().into_dart()].into_dart() + vec![self.0.invoice.into_into_dart().into_dart()].into_dart() } } -impl support::IntoDartExceptPrimitive for LnUrlWithdrawSuccessData {} -impl rust2dart::IntoIntoDart for LnUrlWithdrawSuccessData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_LnUrlWithdrawSuccessData {} +impl rust2dart::IntoIntoDart for LnUrlWithdrawSuccessData { + fn into_into_dart(self) -> mirror_LnUrlWithdrawSuccessData { + mirror_LnUrlWithdrawSuccessData(self) } } @@ -1686,33 +1903,33 @@ impl rust2dart::IntoIntoDart for MaxReverseSwapAmo } } -impl support::IntoDart for MessageSuccessActionData { +impl support::IntoDart for mirror_MessageSuccessActionData { fn into_dart(self) -> support::DartAbi { - vec![self.message.into_into_dart().into_dart()].into_dart() + vec![self.0.message.into_into_dart().into_dart()].into_dart() } } -impl support::IntoDartExceptPrimitive for MessageSuccessActionData {} -impl rust2dart::IntoIntoDart for MessageSuccessActionData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_MessageSuccessActionData {} +impl rust2dart::IntoIntoDart for MessageSuccessActionData { + fn into_into_dart(self) -> mirror_MessageSuccessActionData { + mirror_MessageSuccessActionData(self) } } -impl support::IntoDart for Network { +impl support::IntoDart for mirror_Network { fn into_dart(self) -> support::DartAbi { - match self { - Self::Bitcoin => 0, - Self::Testnet => 1, - Self::Signet => 2, - Self::Regtest => 3, + match self.0 { + Network::Bitcoin => 0, + Network::Testnet => 1, + Network::Signet => 2, + Network::Regtest => 3, } .into_dart() } } -impl support::IntoDartExceptPrimitive for Network {} -impl rust2dart::IntoIntoDart for Network { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_Network {} +impl rust2dart::IntoIntoDart for Network { + fn into_into_dart(self) -> mirror_Network { + mirror_Network(self) } } @@ -1899,7 +2116,7 @@ impl support::IntoDart for PaymentFailedData { vec![ self.error.into_into_dart().into_dart(), self.node_id.into_into_dart().into_dart(), - self.invoice.into_dart(), + self.invoice.map(|v| mirror_LNInvoice(v)).into_dart(), self.label.into_dart(), ] .into_dart() @@ -2137,38 +2354,39 @@ impl rust2dart::IntoIntoDart for ReverseSwapStatus { } } -impl support::IntoDart for RouteHint { +impl support::IntoDart for mirror_RouteHint { fn into_dart(self) -> support::DartAbi { - vec![self.hops.into_into_dart().into_dart()].into_dart() + vec![self.0.hops.into_into_dart().into_dart()].into_dart() } } -impl support::IntoDartExceptPrimitive for RouteHint {} -impl rust2dart::IntoIntoDart for RouteHint { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_RouteHint {} +impl rust2dart::IntoIntoDart for RouteHint { + fn into_into_dart(self) -> mirror_RouteHint { + mirror_RouteHint(self) } } -impl support::IntoDart for RouteHintHop { +impl support::IntoDart for mirror_RouteHintHop { fn into_dart(self) -> support::DartAbi { vec![ - self.src_node_id.into_into_dart().into_dart(), - self.short_channel_id.into_into_dart().into_dart(), - self.fees_base_msat.into_into_dart().into_dart(), - self.fees_proportional_millionths + self.0.src_node_id.into_into_dart().into_dart(), + self.0.short_channel_id.into_into_dart().into_dart(), + self.0.fees_base_msat.into_into_dart().into_dart(), + self.0 + .fees_proportional_millionths .into_into_dart() .into_dart(), - self.cltv_expiry_delta.into_into_dart().into_dart(), - self.htlc_minimum_msat.into_dart(), - self.htlc_maximum_msat.into_dart(), + self.0.cltv_expiry_delta.into_into_dart().into_dart(), + self.0.htlc_minimum_msat.into_dart(), + self.0.htlc_maximum_msat.into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for RouteHintHop {} -impl rust2dart::IntoIntoDart for RouteHintHop { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_RouteHintHop {} +impl rust2dart::IntoIntoDart for RouteHintHop { + fn into_into_dart(self) -> mirror_RouteHintHop { + mirror_RouteHintHop(self) } } @@ -2232,20 +2450,26 @@ impl rust2dart::IntoIntoDart for StaticBackupResponse { } } -impl support::IntoDart for SuccessActionProcessed { +impl support::IntoDart for mirror_SuccessActionProcessed { fn into_dart(self) -> support::DartAbi { - match self { - Self::Aes { result } => vec![0.into_dart(), result.into_into_dart().into_dart()], - Self::Message { data } => vec![1.into_dart(), data.into_into_dart().into_dart()], - Self::Url { data } => vec![2.into_dart(), data.into_into_dart().into_dart()], + match self.0 { + SuccessActionProcessed::Aes { result } => { + vec![0.into_dart(), result.into_into_dart().into_dart()] + } + SuccessActionProcessed::Message { data } => { + vec![1.into_dart(), data.into_into_dart().into_dart()] + } + SuccessActionProcessed::Url { data } => { + vec![2.into_dart(), data.into_into_dart().into_dart()] + } } .into_dart() } } -impl support::IntoDartExceptPrimitive for SuccessActionProcessed {} -impl rust2dart::IntoIntoDart for SuccessActionProcessed { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_SuccessActionProcessed {} +impl rust2dart::IntoIntoDart for SuccessActionProcessed { + fn into_into_dart(self) -> mirror_SuccessActionProcessed { + mirror_SuccessActionProcessed(self) } } @@ -2344,19 +2568,19 @@ impl rust2dart::IntoIntoDart for UnspentTransactionOut } } -impl support::IntoDart for UrlSuccessActionData { +impl support::IntoDart for mirror_UrlSuccessActionData { fn into_dart(self) -> support::DartAbi { vec![ - self.description.into_into_dart().into_dart(), - self.url.into_into_dart().into_dart(), + self.0.description.into_into_dart().into_dart(), + self.0.url.into_into_dart().into_dart(), ] .into_dart() } } -impl support::IntoDartExceptPrimitive for UrlSuccessActionData {} -impl rust2dart::IntoIntoDart for UrlSuccessActionData { - fn into_into_dart(self) -> Self { - self +impl support::IntoDartExceptPrimitive for mirror_UrlSuccessActionData {} +impl rust2dart::IntoIntoDart for UrlSuccessActionData { + fn into_into_dart(self) -> mirror_UrlSuccessActionData { + mirror_UrlSuccessActionData(self) } } diff --git a/libs/sdk-core/src/chain.rs b/libs/sdk-core/src/chain.rs index 4d7ee7053..fe658d449 100644 --- a/libs/sdk-core/src/chain.rs +++ b/libs/sdk-core/src/chain.rs @@ -1,10 +1,10 @@ use anyhow::Result; +use sdk_common::prelude::*; use serde::{Deserialize, Serialize}; use crate::bitcoin::hashes::hex::FromHex; use crate::bitcoin::{OutPoint, Txid}; use crate::error::{SdkError, SdkResult}; -use crate::input_parser::{get_parse_and_log_response, post_and_log_response}; pub const DEFAULT_MEMPOOL_SPACE_URL: &str = "https://mempool.space/api"; @@ -337,20 +337,28 @@ 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 + get_parse_and_log_response(&format!("{}/v1/fees/recommended", self.base_url), true) + .await + .map_err(Into::into) } async fn address_transactions(&self, address: String) -> SdkResult> { - get_parse_and_log_response(&format!("{}/address/{address}/txs", self.base_url), true).await + get_parse_and_log_response(&format!("{}/address/{address}/txs", self.base_url), true) + .await + .map_err(Into::into) } async fn current_tip(&self) -> SdkResult { - get_parse_and_log_response(&format!("{}/blocks/tip/height", self.base_url), true).await + get_parse_and_log_response(&format!("{}/blocks/tip/height", self.base_url), true) + .await + .map_err(Into::into) } 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 + get_parse_and_log_response(&url, true) + .await + .map_err(Into::into) } async fn broadcast_transaction(&self, tx: Vec) -> SdkResult { diff --git a/libs/sdk-core/src/error.rs b/libs/sdk-core/src/error.rs index 929de1817..2b1961f85 100644 --- a/libs/sdk-core/src/error.rs +++ b/libs/sdk-core/src/error.rs @@ -1,11 +1,12 @@ use std::time::SystemTimeError; use anyhow::Result; +use sdk_common::prelude::*; use thiserror::Error; use crate::{ - bitcoin::util::bip32, invoice::InvoiceError, lnurl::error::LnUrlError, node_api::NodeError, - persist::error::PersistError, swap_in::error::SwapError, swap_out::error::ReverseSwapError, + bitcoin::util::bip32, node_api::NodeError, persist::error::PersistError, + swap_in::error::SwapError, swap_out::error::ReverseSwapError, }; pub type SdkResult = Result; @@ -103,100 +104,6 @@ impl From for LnUrlAuthError { } } -/// Error returned by [crate::breez_services::BreezServices::lnurl_pay] -#[derive(Debug, Error)] -pub enum LnUrlPayError { - /// This error is raised when attempting to pay an invoice that has already being paid. - #[error("Invoice already paid")] - AlreadyPaid, - - /// This error is raised when a general error occurs not specific to other error variants - /// in this enum. - #[error("Generic: {err}")] - Generic { err: String }, - - /// This error is raised when the amount from the parsed invoice is not set. - #[error("Invalid amount: {err}")] - InvalidAmount { err: String }, - - /// This error is raised when the lightning invoice cannot be parsed. - #[error("Invalid invoice: {err}")] - InvalidInvoice { err: String }, - - /// This error is raised when the lightning invoice is for a different Bitcoin network. - #[error("Invalid network: {err}")] - InvalidNetwork { err: String }, - - /// This error is raised when the decoded LNURL URI is not compliant to the specification. - #[error("Invalid uri: {err}")] - InvalidUri { err: String }, - - /// This error is raised when the lightning invoice has passed it's expiry time. - #[error("Invoice expired: {err}")] - InvoiceExpired { err: String }, - - /// This error is raised when attempting to make a payment by the node fails. - #[error("Payment failed: {err}")] - PaymentFailed { err: String }, - - /// This error is raised when attempting to make a payment takes too long. - #[error("Payment timeout: {err}")] - PaymentTimeout { err: String }, - - /// This error is raised when no route can be found when attempting to make a - /// payment by the node. - #[error("Route not found: {err}")] - RouteNotFound { err: String }, - - /// This error is raised when the route is considered too expensive when - /// attempting to make a payment by the node. - #[error("Route too expensive: {err}")] - RouteTooExpensive { err: String }, - - /// This error is raised when a connection to an external service fails. - #[error("Service connectivity: {err}")] - ServiceConnectivity { err: String }, -} - -impl From for LnUrlPayError { - fn from(err: anyhow::Error) -> Self { - Self::Generic { - err: err.to_string(), - } - } -} - -impl From for LnUrlPayError { - fn from(err: crate::bitcoin::hashes::hex::Error) -> Self { - Self::Generic { - err: err.to_string(), - } - } -} - -impl From for LnUrlPayError { - fn from(value: InvoiceError) -> Self { - match value { - InvoiceError::InvalidNetwork(err) => Self::InvalidNetwork { err }, - InvoiceError::Validation(err) => Self::InvalidInvoice { err }, - InvoiceError::Generic(err) => Self::Generic { err }, - } - } -} - -impl From for LnUrlPayError { - fn from(value: LnUrlError) -> Self { - match value { - LnUrlError::InvalidUri(err) => Self::InvalidUri { err }, - LnUrlError::InvalidInvoice(err) => err.into(), - LnUrlError::ServiceConnectivity(err) => Self::ServiceConnectivity { err }, - _ => Self::Generic { - err: value.to_string(), - }, - } - } -} - impl From for LnUrlPayError { fn from(err: PersistError) -> Self { Self::Generic { @@ -232,59 +139,6 @@ impl From for LnUrlPayError { } } -/// Error returned by [crate::breez_services::BreezServices::lnurl_withdraw] -#[derive(Debug, Error)] -pub enum LnUrlWithdrawError { - /// This error is raised when a general error occurs not specific to other error variants - /// in this enum. - #[error("Generic: {err}")] - Generic { err: String }, - - /// This error is raised when the amount is zero or the amount does not cover - /// the cost to open a new channel. - #[error("Invalid amount: {err}")] - InvalidAmount { err: String }, - - /// This error is raised when the lightning invoice cannot be parsed. - #[error("Invalid invoice: {err}")] - InvalidInvoice { err: String }, - - /// This error is raised when the decoded LNURL URI is not compliant to the specification. - #[error("Invalid uri: {err}")] - InvalidUri { err: String }, - - /// This error is raised when no routing hints were able to be added to the invoice - /// while trying to receive a payment. - #[error("No routing hints: {err}")] - InvoiceNoRoutingHints { err: String }, - - /// This error is raised when a connection to an external service fails. - #[error("Service connectivity: {err}")] - ServiceConnectivity { err: String }, -} - -impl From for LnUrlWithdrawError { - fn from(value: InvoiceError) -> Self { - match value { - InvoiceError::Validation(err) => Self::InvalidInvoice { err }, - _ => Self::Generic { - err: value.to_string(), - }, - } - } -} - -impl From for LnUrlWithdrawError { - fn from(value: LnUrlError) -> Self { - match value { - LnUrlError::Generic(err) => Self::Generic { err }, - LnUrlError::InvalidUri(err) => Self::InvalidUri { err }, - LnUrlError::InvalidInvoice(err) => err.into(), - LnUrlError::ServiceConnectivity(err) => Self::ServiceConnectivity { err }, - } - } -} - impl From for LnUrlWithdrawError { fn from(err: PersistError) -> Self { Self::Generic { @@ -495,6 +349,12 @@ impl SdkError { } } +impl From for SdkError { + fn from(value: ServiceConnectivityError) -> Self { + Self::ServiceConnectivity { err: value.err } + } +} + impl From for SdkError { fn from(err: anyhow::Error) -> Self { Self::Generic { @@ -794,12 +654,3 @@ impl From for SendPaymentError { } } } - -#[macro_export] -macro_rules! ensure_sdk { - ($cond:expr, $err:expr) => { - if !$cond { - return Err($err); - } - }; -} diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index d4a56ce0e..b49cf9298 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -27,6 +27,7 @@ use gl_client::scheduler::Scheduler; use gl_client::signer::model::greenlight::{amount, scheduler}; use gl_client::signer::{Error, Signer}; use gl_client::{node, utils}; +use sdk_common::prelude::*; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; use tokio::sync::{mpsc, watch, Mutex}; @@ -45,7 +46,6 @@ use crate::bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}; use crate::bitcoin::{ Address, OutPoint, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness, }; -use crate::invoice::{parse_invoice, validate_network, InvoiceError, RouteHintHop}; use crate::lightning::util::message_signing::verify; use crate::lightning_invoice::{RawBolt11Invoice, SignedRawBolt11Invoice}; use crate::node_api::{CreateInvoiceRequest, FetchBolt11Result, NodeAPI, NodeError, NodeResult}; diff --git a/libs/sdk-core/src/lib.rs b/libs/sdk-core/src/lib.rs index 13aa25147..db51b0407 100644 --- a/libs/sdk-core/src/lib.rs +++ b/libs/sdk-core/src/lib.rs @@ -155,13 +155,12 @@ //! //! Join this [telegram group](https://t.me/breezsdk). +#[allow(clippy::all)] mod bridge_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */ + #[macro_use] extern crate log; -#[rustfmt::skip] -#[cfg(test)] -mod test_utils; // flutter_rust_bridge_codegen: has to be defined before breez_services mod backup; pub mod binding; mod breez_services; @@ -177,9 +176,7 @@ mod greenlight; mod grpc; #[rustfmt::skip] mod fiat; // flutter_rust_bridge_codegen: has to be defined after grpc; grpc::Rate -pub mod input_parser; -mod invoice; -mod lnurl; +pub mod lnurl; mod lsp; mod lsps0; mod lsps2; @@ -189,6 +186,10 @@ mod persist; mod support; mod swap_in; mod swap_out; +#[allow(clippy::all)] +#[allow(unused_mut)] +#[allow(dead_code)] +mod test_utils; mod tonic_wrap; // Re-use crates from gl_client for consistency @@ -203,13 +204,7 @@ pub use breez_services::{ }; pub use chain::RecommendedFees; pub use fiat::{CurrencyInfo, FiatCurrency, LocaleOverrides, LocalizedName, Rate, Symbol}; -pub use input_parser::{ - parse, BitcoinAddressData, InputType, LnUrlAuthRequestData, LnUrlErrorData, - LnUrlPayRequestData, LnUrlRequestData, LnUrlWithdrawRequestData, MetadataItem, -}; -pub use invoice::{parse_invoice, LNInvoice, RouteHint, RouteHintHop}; - -pub use lnurl::pay::model::*; pub use lsp::LspInformation; pub use models::*; +pub use sdk_common::prelude::*; pub use swap_out::reverseswap::{ESTIMATED_CLAIM_TX_VSIZE, ESTIMATED_LOCKUP_TX_VSIZE}; diff --git a/libs/sdk-core/src/lnurl/mod.rs b/libs/sdk-core/src/lnurl/mod.rs index c7f795626..d0c0bda84 100644 --- a/libs/sdk-core/src/lnurl/mod.rs +++ b/libs/sdk-core/src/lnurl/mod.rs @@ -1,39 +1,19 @@ -pub(crate) mod auth; -pub(crate) mod error; -pub(crate) mod pay; -pub(crate) mod withdraw; +pub mod pay; -use crate::lnurl::error::LnUrlResult; - -/// 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_mockito_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. - - use self::error::LnUrlError; - - let server = crate::input_parser::tests::MOCK_HTTP_SERVER.lock().unwrap(); - let mockito_endpoint_url = - reqwest::Url::parse(&server.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()))?; +mod tests { + use std::sync::Mutex; - parsed_lnurl_endpoint - .set_host(mockito_endpoint_url.host_str()) - .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - let _ = parsed_lnurl_endpoint.set_scheme(mockito_endpoint_url.scheme()); - let _ = parsed_lnurl_endpoint.set_port(mockito_endpoint_url.port()); - - Ok(parsed_lnurl_endpoint.to_string()) -} + use mockito::Server; + use once_cell::sync::Lazy; -#[cfg(not(test))] -pub(crate) fn maybe_replace_host_with_mockito_test_host( - lnurl_endpoint: String, -) -> LnUrlResult { - // When not called from a test, we fallback to keeping the URL intact - Ok(lnurl_endpoint) + 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 e43a316f3..5d6c62a04 100644 --- a/libs/sdk-core/src/lnurl/pay.rs +++ b/libs/sdk-core/src/lnurl/pay.rs @@ -1,375 +1,49 @@ -use crate::invoice::{parse_invoice, validate_network}; -use crate::lnurl::maybe_replace_host_with_mockito_test_host; -use crate::lnurl::pay::model::{CallbackResponse, SuccessAction, ValidatedCallbackResponse}; -use crate::{ensure_sdk, input_parser::*}; -use crate::{LnUrlErrorData, Network}; -use std::str::FromStr; +use sdk_common::prelude::*; +use serde::Serialize; -use super::error::{LnUrlError, LnUrlResult}; +use crate::Payment; -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; - -/// Validates invoice and performs the second and last step of LNURL-pay, as per -/// +/// Contains the result of the entire LNURL-pay interaction, as reported by the LNURL endpoint. /// -/// See the [parse] docs for more detail on the full workflow. -pub(crate) async fn validate_lnurl_pay( - user_amount_msat: u64, - comment: &Option, - req_data: &LnUrlPayRequestData, - network: Network, -) -> LnUrlResult { - validate_user_input( - user_amount_msat, - comment, - req_data.min_sendable, - req_data.max_sendable, - req_data.comment_allowed, - )?; - - 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) { - Ok(ValidatedCallbackResponse::EndpointError { data: err }) - } else { - let callback_resp: CallbackResponse = serde_json::from_str(&callback_resp_text)?; - if let Some(ref sa) = callback_resp.success_action { - match sa { - SuccessAction::Aes(data) => data.validate()?, - SuccessAction::Message(data) => data.validate()?, - SuccessAction::Url(data) => data.validate(req_data)?, - } - } - - validate_invoice(user_amount_msat, &callback_resp.pr, network)?; - Ok(ValidatedCallbackResponse::EndpointSuccess { - data: callback_resp, - }) - } -} - -fn build_pay_callback_url( - user_amount_msat: u64, - user_comment: &Option, - data: &LnUrlPayRequestData, -) -> LnUrlResult { - let amount_msat = user_amount_msat.to_string(); - let mut url = reqwest::Url::from_str(&data.callback) - .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - - url.query_pairs_mut().append_pair("amount", &amount_msat); - if let Some(comment) = user_comment { - url.query_pairs_mut().append_pair("comment", comment); - } - - let mut callback_url = url.to_string(); - callback_url = maybe_replace_host_with_mockito_test_host(callback_url)?; - Ok(callback_url) -} - -fn validate_user_input( - user_amount_msat: u64, - comment: &Option, - condition_min_amount_msat: u64, - condition_max_amount_msat: u64, - condition_max_comment_len: u16, -) -> LnUrlResult<()> { - ensure_sdk!( - user_amount_msat >= condition_min_amount_msat, - LnUrlError::generic("Amount is smaller than the minimum allowed") - ); - - ensure_sdk!( - user_amount_msat <= condition_max_amount_msat, - LnUrlError::generic("Amount is bigger than the maximum allowed") - ); - - match comment { - None => Ok(()), - Some(msg) => match msg.len() <= condition_max_comment_len as usize { - true => Ok(()), - false => Err(LnUrlError::generic( - "Comment is longer than the maximum allowed comment length", - )), - }, - } +/// * `EndpointSuccess` indicates the payment is complete. The endpoint may return a `SuccessActionProcessed`, +/// in which case, the wallet has to present it to the user as described in +/// +/// +/// * `EndpointError` indicates a generic issue the LNURL endpoint encountered, including a freetext +/// field with the reason. +/// +/// * `PayError` indicates that an error occurred while trying to pay the invoice from the LNURL endpoint. +/// This includes the payment hash of the failed invoice and the failure reason. +#[derive(Serialize)] +#[allow(clippy::large_enum_variant)] +pub enum LnUrlPayResult { + EndpointSuccess { data: LnUrlPaySuccessData }, + EndpointError { data: LnUrlErrorData }, + PayError { data: LnUrlPayErrorData }, } -fn validate_invoice(user_amount_msat: u64, bolt11: &str, network: Network) -> LnUrlResult<()> { - let invoice = parse_invoice(bolt11)?; - // Valid the invoice network against the config network - validate_network(invoice.clone(), network)?; - - match invoice.amount_msat { - None => Err(LnUrlError::generic( - "Amount is bigger than the maximum allowed", - )), - Some(invoice_amount_msat) => match invoice_amount_msat == user_amount_msat { - true => Ok(()), - false => Err(LnUrlError::generic( - "Invoice amount is different than the user chosen amount", - )), - }, - } -} - -pub(crate) mod model { - use crate::lnurl::error::{LnUrlError, LnUrlResult}; - use crate::lnurl::pay::{Aes256CbcDec, Aes256CbcEnc}; - use crate::{ensure_sdk, input_parser::*, Payment}; - - use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; - use anyhow::Result; - use serde::{Deserialize, Serialize}; - - pub(crate) enum ValidatedCallbackResponse { - EndpointSuccess { data: CallbackResponse }, - EndpointError { data: LnUrlErrorData }, - } - - /// Contains the result of the entire LNURL-pay interaction, as reported by the LNURL endpoint. - /// - /// * `EndpointSuccess` indicates the payment is complete. The endpoint may return a `SuccessActionProcessed`, - /// in which case, the wallet has to present it to the user as described in - /// - /// - /// * `EndpointError` indicates a generic issue the LNURL endpoint encountered, including a freetext - /// field with the reason. - /// - /// * `PayError` indicates that an error occurred while trying to pay the invoice from the LNURL endpoint. - /// This includes the payment hash of the failed invoice and the failure reason. - #[derive(Debug, Serialize, Deserialize)] - #[allow(clippy::large_enum_variant)] - pub enum LnUrlPayResult { - EndpointSuccess { data: LnUrlPaySuccessData }, - EndpointError { data: LnUrlErrorData }, - PayError { data: LnUrlPayErrorData }, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct LnUrlPayErrorData { - pub payment_hash: String, - pub reason: String, - } - - #[derive(Serialize, Deserialize, Debug)] - pub struct LnUrlPaySuccessData { - pub payment: Payment, - pub success_action: Option, - } - - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - pub struct CallbackResponse { - pub pr: String, - pub success_action: Option, - } - - /// Payload of the AES success action, as received from the LNURL endpoint - /// - /// See [AesSuccessActionDataDecrypted] for a similar wrapper containing the decrypted payload - #[derive(Deserialize, Debug)] - pub struct AesSuccessActionData { - /// Contents description, up to 144 characters - pub description: String, - - /// Base64, AES-encrypted data where encryption key is payment preimage, up to 4kb of characters - pub ciphertext: String, - - /// Base64, initialization vector, exactly 24 characters - pub iv: String, - } - - /// Result of decryption of [AesSuccessActionData] payload - #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] - pub enum AesSuccessActionDataResult { - Decrypted { data: AesSuccessActionDataDecrypted }, - ErrorStatus { reason: String }, - } - - /// Wrapper for the decrypted [AesSuccessActionData] payload - #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] - pub struct AesSuccessActionDataDecrypted { - /// Contents description, up to 144 characters - pub description: String, - - /// Decrypted content - pub plaintext: String, - } - - #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] - pub struct MessageSuccessActionData { - pub message: String, - } - - #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] - pub struct UrlSuccessActionData { - pub description: String, - pub url: String, - } - - /// [SuccessAction] where contents are ready to be consumed by the caller - /// - /// Contents are identical to [SuccessAction], except for AES where the ciphertext is decrypted. - #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] - pub enum SuccessActionProcessed { - /// See [SuccessAction::Aes] for received payload - /// - /// See [AesSuccessActionDataDecrypted] for decrypted payload - Aes { result: AesSuccessActionDataResult }, - - /// See [SuccessAction::Message] - Message { data: MessageSuccessActionData }, - - /// See [SuccessAction::Url] - Url { data: UrlSuccessActionData }, - } - - /// Supported success action types - /// - /// Receiving any other (unsupported) success action type will result in a failed parsing, - /// which will abort the LNURL-pay workflow, as per LUD-09. - #[derive(Deserialize, Debug)] - #[serde(rename_all = "camelCase")] - #[serde(tag = "tag")] - pub enum SuccessAction { - /// AES type, described in LUD-10 - Aes(AesSuccessActionData), - - /// Message type, described in LUD-09 - Message(MessageSuccessActionData), - - /// URL type, described in LUD-09 - Url(UrlSuccessActionData), - } - - impl AesSuccessActionData { - /// Validates the fields, but does not decrypt and validate the ciphertext. - pub fn validate(&self) -> LnUrlResult<()> { - ensure_sdk!( - self.description.len() <= 144, - LnUrlError::generic( - "AES action description length is larger than the maximum allowed" - ) - ); - - ensure_sdk!( - self.ciphertext.len() <= 4096, - LnUrlError::generic( - "AES action ciphertext length is larger than the maximum allowed" - ) - ); - - base64::decode(&self.ciphertext)?; - - ensure_sdk!( - self.iv.len() == 24, - LnUrlError::generic("AES action iv has unexpected length") - ); - - base64::decode(&self.iv)?; - - Ok(()) - } - - /// Decrypts the ciphertext as a UTF-8 string, given the key (invoice preimage) parameter. - pub fn decrypt(&self, key: &[u8; 32]) -> Result { - let plaintext_bytes = - Aes256CbcDec::new_from_slices(key, &base64::decode(&self.iv)?)? - .decrypt_padded_vec_mut::(&base64::decode(&self.ciphertext)?)?; - - Ok(String::from_utf8(plaintext_bytes)?) - } - - /// Helper method that encrypts a given plaintext, with a given key and IV. - pub fn encrypt(key: &[u8; 32], iv: &[u8; 16], plaintext: String) -> Result { - let ciphertext_bytes = Aes256CbcEnc::new_from_slices(key, iv)? - .encrypt_padded_vec_mut::(plaintext.as_bytes()); - - Ok(base64::encode(ciphertext_bytes)) - } - } - - impl TryFrom<(AesSuccessActionData, &[u8; 32])> for AesSuccessActionDataDecrypted { - type Error = anyhow::Error; - - fn try_from( - value: (AesSuccessActionData, &[u8; 32]), - ) -> std::result::Result { - let data = value.0; - let key = value.1; - - Ok(AesSuccessActionDataDecrypted { - description: data.description.clone(), - plaintext: data.decrypt(key)?, - }) - } - } - - impl MessageSuccessActionData { - pub fn validate(&self) -> LnUrlResult<()> { - match self.message.len() <= 144 { - true => Ok(()), - false => Err(LnUrlError::generic( - "Success action message is longer than the maximum allowed length", - )), - } - } - } - - impl UrlSuccessActionData { - pub fn validate(&self, data: &LnUrlPayRequestData) -> LnUrlResult<()> { - match self.description.len() <= 144 { - true => Ok(()), - false => Err(LnUrlError::generic( - "Success action description is longer than the maximum allowed length", - )), - } - .and_then(|_| { - let req_url = reqwest::Url::parse(&data.callback) - .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - let req_domain = req_url.domain().ok_or_else(|| { - LnUrlError::invalid_uri("Could not determine callback domain") - })?; - - let action_res_url = reqwest::Url::parse(&self.url) - .map_err(|e| LnUrlError::InvalidUri(e.to_string()))?; - let action_res_domain = action_res_url.domain().ok_or_else(|| { - LnUrlError::invalid_uri("Could not determine Success Action URL domain") - })?; - - match req_domain == action_res_domain { - true => Ok(()), - false => Err(LnUrlError::generic( - "Success Action URL has different domain than the callback domain", - )), - } - }) - } - } +#[derive(Serialize)] +pub struct LnUrlPaySuccessData { + pub payment: Payment, + pub success_action: Option, } #[cfg(test)] -mod tests { +pub(crate) mod tests { use std::sync::Arc; - use crate::bitcoin::hashes::hex::ToHex; - use crate::bitcoin::hashes::{sha256, Hash}; - use crate::input_parser::tests::MOCK_HTTP_SERVER; - use crate::lnurl::pay::*; - use crate::{breez_services::tests::get_dummy_node_state, lnurl::pay::model::*}; - use crate::{test_utils::*, LnUrlPayRequest}; - - use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use anyhow::{anyhow, Result}; + use gl_client::bitcoin::hashes::hex::ToHex; use gl_client::signer::model::greenlight::PayStatus; use mockito::Mock; use rand::random; + use crate::bitcoin::hashes::{sha256, Hash}; + use crate::breez_services::tests::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, @@ -541,7 +215,7 @@ mod tests { "successAction": { "tag":"url", "description":"test description", - "url":"https://localhost/test-url" + "url":"http://localhost:8080/test-url" } } "# @@ -631,7 +305,7 @@ mod tests { max_sendable, comment_allowed: comment_len, metadata_str: "".into(), - callback: "https://localhost/callback".into(), + callback: "http://localhost:8080/callback".into(), domain: "localhost".into(), allows_nostr: false, nostr_pubkey: None, @@ -639,18 +313,6 @@ mod tests { } } - #[test] - fn test_lnurl_pay_validate_input() -> Result<()> { - assert!(validate_user_input(100_000, &None, 0, 100_000, 0).is_ok()); - assert!(validate_user_input(100_000, &Some("test".into()), 0, 100_000, 5).is_ok()); - - assert!(validate_user_input(5000, &None, 10_000, 100_000, 5).is_err()); - assert!(validate_user_input(200_000, &None, 10_000, 100_000, 5).is_err()); - assert!(validate_user_input(100_000, &Some("test".into()), 10_000, 100_000, 0).is_err()); - - Ok(()) - } - #[test] fn test_lnurl_pay_validate_invoice() -> Result<()> { let req = get_test_pay_req_data(0, 100_000, 0); @@ -714,165 +376,6 @@ mod tests { Ok(()) } - #[test] - fn test_lnurl_pay_validate_success_action_encrypt_decrypt() -> Result<()> { - // Simulate a preimage, which will be the AES key - let key = sha256::Hash::hash(&[0x42; 16]); - let key_bytes = key.as_inner(); - - let iv_bytes = [0x24; 16]; // 16 bytes = 24 chars - let iv_base64 = base64::encode(iv_bytes); // JCQkJCQkJCQkJCQkJCQkJA== - - let plaintext = "hello world! this is my plaintext."; - let plaintext_bytes = plaintext.as_bytes(); - - // hex = 91239ab5d94369a18474ee58372c7d0fcee5e227903f671bfe19ef32f1cada804d10f0f006265289d936317343dbc0ca - // base64 = kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK - let ciphertext_bytes = - &hex::decode("91239ab5d94369a18474ee58372c7d0fcee5e227903f671bfe19ef32f1cada804d10f0f006265289d936317343dbc0ca")?; - let ciphertext_base64 = base64::encode(ciphertext_bytes); - - // Encrypt raw (which returns raw bytes) - let res = Aes256CbcEnc::new_from_slices(key_bytes, &iv_bytes)? - .encrypt_padded_vec_mut::(plaintext_bytes); - assert_eq!(res[..], ciphertext_bytes[..]); - - // Decrypt raw (which returns raw bytes) - let res = Aes256CbcDec::new_from_slices(key_bytes, &iv_bytes)? - .decrypt_padded_vec_mut::(&res)?; - assert_eq!(res[..], plaintext_bytes[..]); - - // Encrypt via AesSuccessActionData helper method (which returns a base64 representation of the bytes) - let res = AesSuccessActionData::encrypt(key_bytes, &iv_bytes, plaintext.into())?; - assert_eq!(res, base64::encode(ciphertext_bytes)); - - // Decrypt via AesSuccessActionData instance method (which returns an UTF-8 string of the plaintext bytes) - let res = AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: ciphertext_base64, - iv: iv_base64, - } - .decrypt(key_bytes)?; - assert_eq!(res.as_bytes(), plaintext_bytes); - - Ok(()) - } - - #[test] - fn test_lnurl_pay_validate_success_action_aes() -> Result<()> { - assert!(AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), - iv: base64::encode([0xa; 16]) - } - .validate() - .is_ok()); - - // Description longer than 144 chars - assert!(AesSuccessActionData { - description: rand_string(150), - ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), - iv: base64::encode([0xa; 16]) - } - .validate() - .is_err()); - - // IV size below 16 bytes (24 chars) - assert!(AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), - iv: base64::encode([0xa; 10]) - } - .validate() - .is_err()); - - // IV size above 16 bytes (24 chars) - assert!(AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), - iv: base64::encode([0xa; 20]) - } - .validate() - .is_err()); - - // IV is not base64 encoded (but fits length of 24 chars) - assert!(AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: "kSOatdlDaaGEdO5YNyx9D87l4ieQP2cb/hnvMvHK2oBNEPDwBiZSidk2MXND28DK".into(), - iv: ",".repeat(24) - } - .validate() - .is_err()); - - // Ciphertext is not base64 encoded - assert!(AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: ",".repeat(96), - iv: base64::encode([0xa; 16]) - } - .validate() - .is_err()); - - // Ciphertext longer than 4KB - assert!(AesSuccessActionData { - description: "Test AES successData description".into(), - ciphertext: base64::encode(rand_string(5000)), - iv: base64::encode([0xa; 16]) - } - .validate() - .is_err()); - - Ok(()) - } - - #[test] - fn test_lnurl_pay_validate_success_action_msg() -> Result<()> { - assert!(MessageSuccessActionData { - message: "short msg".into() - } - .validate() - .is_ok()); - - // Too long message - assert!(MessageSuccessActionData { - message: rand_string(150) - } - .validate() - .is_err()); - - Ok(()) - } - - #[test] - fn test_lnurl_pay_validate_success_url() -> Result<()> { - let pay_req_data = get_test_pay_req_data(0, 100_000, 100); - - assert!(UrlSuccessActionData { - description: "short msg".into(), - url: pay_req_data.callback.clone() - } - .validate(&pay_req_data) - .is_ok()); - - // Too long description - assert!(UrlSuccessActionData { - description: rand_string(150), - url: pay_req_data.callback.clone() - } - .validate(&pay_req_data) - .is_err()); - - // Different Success Action domain than in the callback URL - assert!(UrlSuccessActionData { - description: "short msg".into(), - url: "https://new-domain.com/test-url".into() - } - .validate(&pay_req_data) - .is_err()); - - Ok(()) - } - #[tokio::test] async fn test_lnurl_pay_no_success_action() -> Result<()> { let comment = rand_string(COMMENT_LENGTH as usize); @@ -1128,7 +631,8 @@ mod tests { .. }, } => { - if url.url == "https://localhost/test-url" && url.description == "test description" + if url.url == "http://localhost:8080/test-url" + && url.description == "test description" { Ok(()) } else { @@ -1292,10 +796,10 @@ mod tests { { LnUrlPayResult::EndpointSuccess { data: - LnUrlPaySuccessData { - success_action: Some(received_sa), - .. - }, + LnUrlPaySuccessData { + success_action: Some(received_sa), + .. + }, } => match received_sa == sa { true => Ok(()), false => Err(anyhow!( @@ -1304,10 +808,10 @@ mod tests { }, LnUrlPayResult::EndpointSuccess { data: - LnUrlPaySuccessData { - success_action: None, - .. - }, + LnUrlPaySuccessData { + success_action: None, + .. + }, } => Err(anyhow!( "Expected success action in callback, but none provided" )), diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index 9de57723d..5882628e5 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -8,6 +8,8 @@ use ripemd::Digest; use ripemd::Ripemd160; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; use rusqlite::ToSql; +use sdk_common::prelude::Network::*; +use sdk_common::prelude::*; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; @@ -18,22 +20,18 @@ use crate::bitcoin::hashes::{sha256, Hash}; use crate::bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use crate::bitcoin::{Address, Script}; use crate::breez_services::BreezServer; +use crate::ensure_sdk; use crate::error::SdkResult; use crate::fiat::{FiatCurrency, Rate}; use crate::grpc::{ self, GetReverseRoutingNodeRequest, PaymentInformation, RegisterPaymentNotificationResponse, RegisterPaymentReply, RemovePaymentNotificationResponse, }; -use crate::lnurl::pay::model::SuccessActionProcessed; use crate::lsp::LspInformation; -use crate::models::Network::*; use crate::persist::swap::SwapChainInfo; use crate::swap_in::error::{SwapError, SwapResult}; use crate::swap_out::boltzswap::{BoltzApiCreateReverseSwapResponse, BoltzApiReverseSwapStatus}; use crate::swap_out::error::{ReverseSwapError, ReverseSwapResult}; -use crate::{ - ensure_sdk, LNInvoice, LnUrlErrorData, LnUrlPayRequestData, LnUrlWithdrawRequestData, RouteHint, -}; pub const SWAP_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60 * 24 * 2; // 2 days pub const INVOICE_PAYMENT_FEE_EXPIRY_SECONDS: u32 = 60 * 60; // 60 minutes @@ -574,38 +572,6 @@ pub struct GreenlightDeviceCredentials { pub device: Vec, } -/// The different supported bitcoin networks -#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, Serialize, Deserialize)] -pub enum Network { - /// Mainnet - Bitcoin, - Testnet, - Signet, - Regtest, -} - -impl From for Network { - fn from(network: crate::bitcoin::network::constants::Network) -> Self { - match network { - crate::bitcoin::network::constants::Network::Bitcoin => Bitcoin, - crate::bitcoin::network::constants::Network::Testnet => Testnet, - crate::bitcoin::network::constants::Network::Signet => Signet, - crate::bitcoin::network::constants::Network::Regtest => Regtest, - } - } -} - -impl From for crate::bitcoin::network::constants::Network { - fn from(network: Network) -> Self { - match network { - Bitcoin => crate::bitcoin::network::constants::Network::Bitcoin, - Testnet => crate::bitcoin::network::constants::Network::Testnet, - Signet => crate::bitcoin::network::constants::Network::Signet, - Regtest => crate::bitcoin::network::constants::Network::Regtest, - } - } -} - /// Represents a configure node request. #[derive(Default)] pub struct ConfigureNodeRequest { @@ -1547,69 +1513,6 @@ pub struct UnspentTransactionOutput { pub reserved: bool, } -/// Contains the result of the entire LNURL interaction, as reported by the LNURL endpoint. -/// -/// * `Ok` indicates the interaction with the endpoint was valid, and the endpoint -/// - started to pay the invoice asynchronously in the case of LNURL-withdraw, -/// - verified the client signature in the case of LNURL-auth,////// * `Error` indicates a generic issue the LNURL endpoint encountered, including a freetext -/// description of the reason. -/// -/// Both cases are described in LUD-03 & LUD-04: -#[derive(Deserialize, Debug, Serialize)] -#[serde(rename_all = "UPPERCASE")] -#[serde(tag = "status")] -pub enum LnUrlCallbackStatus { - /// On-wire format is: `{"status": "OK"}` - Ok, - /// On-wire format is: `{"status": "ERROR", "reason": "error details..."}` - #[serde(rename = "ERROR")] - ErrorStatus { - #[serde(flatten)] - data: LnUrlErrorData, - }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LnUrlWithdrawRequest { - /// Request data containing information on how to call the lnurl withdraw - /// endpoint. Typically retrieved by calling `parse()` on a lnurl withdraw - /// input. - pub data: LnUrlWithdrawRequestData, - - /// The amount to withdraw from the lnurl withdraw endpoint. Must be between - /// `min_withdrawable` and `max_withdrawable`. - pub amount_msat: u64, - - /// Optional description that will be put in the payment request for the - /// lnurl withdraw endpoint. - pub description: Option, -} - -/// Represents a LNURL-pay request. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LnUrlPayRequest { - /// The [LnUrlPayRequestData] returned by [crate::input_parser::parse] - pub data: LnUrlPayRequestData, - /// The amount in millisatoshis for this payment - pub amount_msat: u64, - /// An optional comment for this payment - pub comment: Option, - /// The external label or identifier of the [Payment] - pub payment_label: Option, -} - -/// [LnUrlCallbackStatus] specific to LNURL-withdraw, where the success case contains the invoice. -#[derive(Serialize)] -pub enum LnUrlWithdrawResult { - Ok { data: LnUrlWithdrawSuccessData }, - ErrorStatus { data: LnUrlErrorData }, -} - -#[derive(Deserialize, Debug, Serialize)] -pub struct LnUrlWithdrawSuccessData { - pub invoice: LNInvoice, -} - /// Different providers will demand different behaviours when the user is trying to buy bitcoin. #[derive(PartialEq, Eq, Debug, Clone, Deserialize, Serialize)] #[serde(tag = "buy_bitcoin_provider")] diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs index f54ce3de7..bfd664890 100644 --- a/libs/sdk-core/src/node_api.rs +++ b/libs/sdk-core/src/node_api.rs @@ -5,15 +5,17 @@ use tokio::sync::{mpsc, watch}; use tokio_stream::Stream; use tonic::Streaming; +use sdk_common::prelude::*; + use crate::{ bitcoin::util::bip32::{ChildNumber, ExtendedPrivKey}, - invoice::InvoiceError, lightning_invoice::RawBolt11Invoice, persist::error::PersistError, CustomMessage, LspInformation, MaxChannelAmount, NodeCredentials, Payment, PaymentResponse, PrepareRedeemOnchainFundsRequest, PrepareRedeemOnchainFundsResponse, RouteHint, RouteHintHop, SyncResponse, TlvEntry, }; +use crate::error::LnUrlAuthError; pub type NodeResult = Result; @@ -69,6 +71,27 @@ impl NodeError { } } +impl From for sdk_common::prelude::LnUrlError { + fn from(value: NodeError) -> Self { + match value { + NodeError::InvalidInvoice(err) => Self::InvalidInvoice(format!("{err}")), + NodeError::ServiceConnectivity(err) => Self::ServiceConnectivity(err), + _ => Self::Generic(value.to_string()), + } + } +} + +impl From for LnUrlAuthError { + fn from(value: NodeError) -> Self { + match value { + NodeError::ServiceConnectivity(err) => Self::ServiceConnectivity { err }, + _ => Self::Generic { + err: value.to_string() + }, + } + } +} + pub struct CreateInvoiceRequest { pub amount_msat: u64, pub description: String, diff --git a/libs/sdk-core/src/persist/transactions.rs b/libs/sdk-core/src/persist/transactions.rs index 699b173d1..7c28a9403 100644 --- a/libs/sdk-core/src/persist/transactions.rs +++ b/libs/sdk-core/src/persist/transactions.rs @@ -1,14 +1,15 @@ -use super::db::SqliteStorage; -use super::error::{PersistError, PersistResult}; -use crate::lnurl::pay::model::SuccessActionProcessed; -use crate::{ensure_sdk, models::*}; +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; use rusqlite::Row; use rusqlite::{named_params, params, OptionalExtension}; -use std::collections::{HashMap, HashSet}; - +use sdk_common::prelude::*; use serde_json::{Map, Value}; -use std::str::FromStr; + +use super::db::SqliteStorage; +use super::error::{PersistError, PersistResult}; +use crate::models::*; const METADATA_MAX_LEN: usize = 1000; @@ -503,24 +504,10 @@ impl ToSql for PaymentStatus { } } -impl FromSql for SuccessActionProcessed { - fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult { - serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType) - } -} - -impl ToSql for SuccessActionProcessed { - fn to_sql(&self) -> rusqlite::Result> { - Ok(ToSqlOutput::from( - serde_json::to_string(&self).map_err(|_| FromSqlError::InvalidType)?, - )) - } -} - #[test] fn test_ln_transactions() -> PersistResult<(), Box> { - use crate::lnurl::pay::model::MessageSuccessActionData; - use crate::lnurl::pay::model::SuccessActionProcessed; + use sdk_common::prelude::*; + use crate::models::{LnPaymentDetails, Payment, PaymentDetails}; use crate::persist::test_utils; diff --git a/libs/sdk-core/src/swap_out/boltzswap.rs b/libs/sdk-core/src/swap_out/boltzswap.rs index 3afcd7854..8cabfbb3a 100644 --- a/libs/sdk-core/src/swap_out/boltzswap.rs +++ b/libs/sdk-core/src/swap_out/boltzswap.rs @@ -6,10 +6,11 @@ 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::input_parser::{get_parse_and_log_response, get_reqwest_client}; +use crate::error::SdkError; use crate::models::ReverseSwapPairInfo; use crate::swap_out::reverseswap::CreateReverseSwapResponse; use crate::{ReverseSwapServiceAPI, RouteHint, RouteHintHop}; @@ -242,7 +243,8 @@ impl ReverseSwapServiceAPI for BoltzApi { pair_hash: String, routing_node: String, ) -> ReverseSwapResult { - get_reqwest_client()? + get_reqwest_client() + .map_err(SdkError::from)? .post(CREATE_REVERSE_SWAP_ENDPOINT) .header(CONTENT_TYPE, "application/json") .body(build_boltz_reverse_swap_args( @@ -282,7 +284,8 @@ 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()? + 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())) @@ -307,7 +310,8 @@ impl ReverseSwapServiceAPI for BoltzApi { } async fn get_route_hints(&self, routing_node_id: String) -> ReverseSwapResult> { - get_reqwest_client()? + get_reqwest_client() + .map_err(SdkError::from)? .post(GET_ROUTE_HINTS_ENDPOINT) .header(CONTENT_TYPE, "application/json") .body(Body::from( @@ -340,7 +344,9 @@ 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?; + 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) => { diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs index 605940201..67e5febaf 100644 --- a/libs/sdk-core/src/test_utils.rs +++ b/libs/sdk-core/src/test_utils.rs @@ -214,8 +214,8 @@ impl Default for MockChainService { .unwrap(); let txs: Vec = serde_json::from_str( - r#"[{"txid":"a418e856bb22b6345868dc0b1ac1dd7a6b7fae1d231b275b74172f9584fa0bdf","version":1,"locktime":0,"vin":[{"txid":"ec901bcab07df7d475d98fff2933dcb56d57bbdaa029c4142aed93462b6928fe","vout":0,"prevout":{"scriptpubkey":"0014b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5","value":197497253},"scriptsig":"","scriptsig_asm":"","witness":["304502210089933e46614114e060d3d681c54af71e3d47f8be8131d9310ef8fe231c060f3302204103910a6790e3a678964df6f0f9ae2107666a91e777bd87f9172a28653e374701","0356f385879fefb8c52758126f6e7b9ac57374c2f73f2ee9047b4c61df0ba390b9"],"is_coinbase":false,"sequence":4294967293},{"txid":"fda3ce37f5fb849502e2027958d51efebd1841cb43bbfdd5f3d354c93a551ef9","vout":0,"prevout":{"scriptpubkey":"00145c7f3b6ceb79d03d5a5397df83f2334394ebdd2c","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 5c7f3b6ceb79d03d5a5397df83f2334394ebdd2c","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt3lnkm8t08gr6kjnjl0c8u3ngw2whhfvzwsxrg","value":786885},"scriptsig":"","scriptsig_asm":"","witness":["304402200ae5465efe824609f7faf1094cce0195763df52e5409dd9ae0526568bf3bcaa20220103749041a87e082cf95bf1e12c5174881e5e4c55e75ab2db29a68538dbabbad01","03dfd8cc1f72f46d259dc0afc6d756bce551fce2fbf58a9ad36409a1b82a17e64f"],"is_coinbase":false,"sequence":4294967293}],"vout":[{"scriptpubkey":"a9141df45814863edfd6d87457e8f8bd79607a116a8f87","scriptpubkey_asm":"OP_HASH160 OP_PUSHBYTES_20 1df45814863edfd6d87457e8f8bd79607a116a8f OP_EQUAL","scriptpubkey_type":"p2sh","scriptpubkey_address":"34RQERthXaruAXtW6q1bvrGTeUbqi2Sm1i","value":26087585},{"scriptpubkey":"001479001aa5f4b981a0b654c3f834d0573595b0ed53","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 79001aa5f4b981a0b654c3f834d0573595b0ed53","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q0yqp4f05hxq6pdj5c0urf5zhxk2mpm2ndx85za","value":171937413}],"size":372,"weight":837,"fee":259140,"status":{"confirmed":true,"block_height":767637,"block_hash":"000000000000000000077769f3b2e6a28b9ed688f0d773f9ff2d73c622a2cfac","block_time":1671174562}},{"txid":"ec901bcab07df7d475d98fff2933dcb56d57bbdaa029c4142aed93462b6928fe","version":1,"locktime":767636,"vin":[{"txid":"d4344fc9e7f66b3a1a50d1d76836a157629ba0c6ede093e94f1c809d334c9146","vout":0,"prevout":{"scriptpubkey":"0014cab22290b7adc75f861de820baa97d319c1110a6","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 cab22290b7adc75f861de820baa97d319c1110a6","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qe2ez9y9h4hr4lpsaaqst42taxxwpzy9xlzqt8k","value":209639471},"scriptsig":"","scriptsig_asm":"","witness":["304402202e914c35b75da798f0898c7cfe6ead207aaee41219afd77124fd56971f05d9030220123ce5d124f4635171b7622995dae35e00373a5fbf8117bfdca5e5080ad6554101","02122fa6d20413bb5da5c7e3fb42228be5436b1bd84e29b294bfc200db5eac460e"],"is_coinbase":false,"sequence":4294967293}],"vout":[{"scriptpubkey":"0014b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5","value":197497253},{"scriptpubkey":"0014f0e2a057d0e60411ac3d7218e29bf9489a59df18","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0e2a057d0e60411ac3d7218e29bf9489a59df18","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7r32q47suczprtpawgvw9xlefzd9nhccyatxvu","value":12140465}],"size":222,"weight":561,"fee":1753,"status":{"confirmed":true,"block_height":767637,"block_hash":"000000000000000000077769f3b2e6a28b9ed688f0d773f9ff2d73c622a2cfac","block_time":1671174562}}]"#, - ).unwrap(); + r#"[{"txid":"a418e856bb22b6345868dc0b1ac1dd7a6b7fae1d231b275b74172f9584fa0bdf","version":1,"locktime":0,"vin":[{"txid":"ec901bcab07df7d475d98fff2933dcb56d57bbdaa029c4142aed93462b6928fe","vout":0,"prevout":{"scriptpubkey":"0014b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5","value":197497253},"scriptsig":"","scriptsig_asm":"","witness":["304502210089933e46614114e060d3d681c54af71e3d47f8be8131d9310ef8fe231c060f3302204103910a6790e3a678964df6f0f9ae2107666a91e777bd87f9172a28653e374701","0356f385879fefb8c52758126f6e7b9ac57374c2f73f2ee9047b4c61df0ba390b9"],"is_coinbase":false,"sequence":4294967293},{"txid":"fda3ce37f5fb849502e2027958d51efebd1841cb43bbfdd5f3d354c93a551ef9","vout":0,"prevout":{"scriptpubkey":"00145c7f3b6ceb79d03d5a5397df83f2334394ebdd2c","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 5c7f3b6ceb79d03d5a5397df83f2334394ebdd2c","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qt3lnkm8t08gr6kjnjl0c8u3ngw2whhfvzwsxrg","value":786885},"scriptsig":"","scriptsig_asm":"","witness":["304402200ae5465efe824609f7faf1094cce0195763df52e5409dd9ae0526568bf3bcaa20220103749041a87e082cf95bf1e12c5174881e5e4c55e75ab2db29a68538dbabbad01","03dfd8cc1f72f46d259dc0afc6d756bce551fce2fbf58a9ad36409a1b82a17e64f"],"is_coinbase":false,"sequence":4294967293}],"vout":[{"scriptpubkey":"a9141df45814863edfd6d87457e8f8bd79607a116a8f87","scriptpubkey_asm":"OP_HASH160 OP_PUSHBYTES_20 1df45814863edfd6d87457e8f8bd79607a116a8f OP_EQUAL","scriptpubkey_type":"p2sh","scriptpubkey_address":"34RQERthXaruAXtW6q1bvrGTeUbqi2Sm1i","value":26087585},{"scriptpubkey":"001479001aa5f4b981a0b654c3f834d0573595b0ed53","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 79001aa5f4b981a0b654c3f834d0573595b0ed53","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q0yqp4f05hxq6pdj5c0urf5zhxk2mpm2ndx85za","value":171937413}],"size":372,"weight":837,"fee":259140,"status":{"confirmed":true,"block_height":767637,"block_hash":"000000000000000000077769f3b2e6a28b9ed688f0d773f9ff2d73c622a2cfac","block_time":1671174562}},{"txid":"ec901bcab07df7d475d98fff2933dcb56d57bbdaa029c4142aed93462b6928fe","version":1,"locktime":767636,"vin":[{"txid":"d4344fc9e7f66b3a1a50d1d76836a157629ba0c6ede093e94f1c809d334c9146","vout":0,"prevout":{"scriptpubkey":"0014cab22290b7adc75f861de820baa97d319c1110a6","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 cab22290b7adc75f861de820baa97d319c1110a6","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qe2ez9y9h4hr4lpsaaqst42taxxwpzy9xlzqt8k","value":209639471},"scriptsig":"","scriptsig_asm":"","witness":["304402202e914c35b75da798f0898c7cfe6ead207aaee41219afd77124fd56971f05d9030220123ce5d124f4635171b7622995dae35e00373a5fbf8117bfdca5e5080ad6554101","02122fa6d20413bb5da5c7e3fb42228be5436b1bd84e29b294bfc200db5eac460e"],"is_coinbase":false,"sequence":4294967293}],"vout":[{"scriptpubkey":"0014b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 b34b7da80e662d1db3fcfbe34b7f4cacc4fac34d","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1qkd9hm2qwvck3mvlul035kl6v4nz04s6dmryeq5","value":197497253},{"scriptpubkey":"0014f0e2a057d0e60411ac3d7218e29bf9489a59df18","scriptpubkey_asm":"OP_0 OP_PUSHBYTES_20 f0e2a057d0e60411ac3d7218e29bf9489a59df18","scriptpubkey_type":"v0_p2wpkh","scriptpubkey_address":"bc1q7r32q47suczprtpawgvw9xlefzd9nhccyatxvu","value":12140465}],"size":222,"weight":561,"fee":1753,"status":{"confirmed":true,"block_height":767637,"block_hash":"000000000000000000077769f3b2e6a28b9ed688f0d773f9ff2d73c622a2cfac","block_time":1671174562}}]"#, + ).unwrap(); Self { tip: 767640, recommended_fees, diff --git a/libs/sdk-flutter/ios/Classes/bridge_generated.h b/libs/sdk-flutter/ios/Classes/bridge_generated.h index a657bd696..61e733af5 100644 --- a/libs/sdk-flutter/ios/Classes/bridge_generated.h +++ b/libs/sdk-flutter/ios/Classes/bridge_generated.h @@ -11,6 +11,10 @@ typedef struct _Dart_Handle* Dart_Handle; #define ESTIMATED_LOCKUP_TX_VSIZE 153 +#define MOCK_REVERSE_SWAP_MIN 50000 + +#define MOCK_REVERSE_SWAP_MAX 1000000 + typedef struct DartCObject DartCObject; typedef int64_t DartPort; diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index 3d268122f..984d2f5e9 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -324,12 +324,8 @@ abstract class BreezSdkCore { FlutterRustBridgeTaskConstMeta get kGenerateDiagnosticDataConstMeta; } -/// Wrapper for the decrypted [AesSuccessActionData] payload class AesSuccessActionDataDecrypted { - /// Contents description, up to 144 characters final String description; - - /// Decrypted content final String plaintext; const AesSuccessActionDataDecrypted({ @@ -368,7 +364,6 @@ class BackupStatus { }); } -/// Wrapped in a [BitcoinAddress], this is the result of [parse] when given a plain or BIP-21 BTC address. class BitcoinAddressData { final String address; final Network network; @@ -651,16 +646,9 @@ enum HealthCheckStatus { @freezed sealed class InputType with _$InputType { - /// # Supported standards - /// - /// - plain on-chain BTC address - /// - BIP21 const factory InputType.bitcoinAddress({ required BitcoinAddressData address, }) = InputType_BitcoinAddress; - - /// Also covers URIs like `bitcoin:...&lightning=bolt11`. In this case, it returns the BOLT11 - /// and discards all other data. const factory InputType.bolt11({ required LNInvoice invoice, }) = InputType_Bolt11; @@ -670,36 +658,12 @@ sealed class InputType with _$InputType { const factory InputType.url({ required String url, }) = InputType_Url; - - /// # Supported standards - /// - /// - LUD-01 LNURL bech32 encoding - /// - LUD-06 `payRequest` spec - /// - LUD-16 LN Address - /// - LUD-17 Support for lnurlp prefix with non-bech32-encoded LNURL URLs const factory InputType.lnUrlPay({ required LnUrlPayRequestData data, }) = InputType_LnUrlPay; - - /// # Supported standards - /// - /// - LUD-01 LNURL bech32 encoding - /// - LUD-03 `withdrawRequest` spec - /// - LUD-17 Support for lnurlw prefix with non-bech32-encoded LNURL URLs - /// - /// # Not supported (yet) - /// - /// - LUD-14 `balanceCheck`: reusable `withdrawRequest`s - /// - LUD-19 Pay link discoverable from withdraw link const factory InputType.lnUrlWithdraw({ required LnUrlWithdrawRequestData data, }) = InputType_LnUrlWithdraw; - - /// # Supported standards - /// - /// - LUD-01 LNURL bech32 encoding - /// - LUD-04 `auth` base spec - /// - LUD-17 Support for keyauth prefix with non-bech32-encoded LNURL URLs const factory InputType.lnUrlAuth({ required LnUrlAuthRequestData data, }) = InputType_LnUrlAuth; @@ -746,7 +710,6 @@ class ListPaymentsRequest { }); } -/// Wrapper for a BOLT11 LN invoice class LNInvoice { final String bolt11; final Network network; @@ -838,24 +801,10 @@ class LnPaymentDetails { }); } -/// Wrapped in a [LnUrlAuth], this is the result of [parse] when given a LNURL-auth endpoint. -/// -/// It represents the endpoint's parameters for the LNURL workflow. -/// -/// See class LnUrlAuthRequestData { - /// Hex encoded 32 bytes of challenge final String k1; - - /// When available, one of: register, login, link, auth final String? action; - - /// Indicates the domain of the LNURL-auth service, to be shown to the user when asking for - /// auth confirmation, as per LUD-04 spec. final String domain; - - /// Indicates the URL of the LNURL-auth service, including the query arguments. This will be - /// extended with the signed challenge and the linking key, then called in the second step of the workflow. final String url; const LnUrlAuthRequestData({ @@ -868,16 +817,12 @@ class LnUrlAuthRequestData { @freezed sealed class LnUrlCallbackStatus with _$LnUrlCallbackStatus { - /// On-wire format is: `{"status": "OK"}` const factory LnUrlCallbackStatus.ok() = LnUrlCallbackStatus_Ok; - - /// On-wire format is: `{"status": "ERROR", "reason": "error details..."}` const factory LnUrlCallbackStatus.errorStatus({ required LnUrlErrorData data, }) = LnUrlCallbackStatus_ErrorStatus; } -/// Wrapped in a [LnUrlError], this represents a LNURL-endpoint error. class LnUrlErrorData { final String reason; @@ -896,18 +841,10 @@ class LnUrlPayErrorData { }); } -/// Represents a LNURL-pay request. class LnUrlPayRequest { - /// The [LnUrlPayRequestData] returned by [crate::input_parser::parse] final LnUrlPayRequestData data; - - /// The amount in millisatoshis for this payment final int amountMsat; - - /// An optional comment for this payment final String? comment; - - /// The external label or identifier of the [Payment] final String? paymentLabel; const LnUrlPayRequest({ @@ -918,48 +855,15 @@ class LnUrlPayRequest { }); } -/// Wrapped in a [LnUrlPay], this is the result of [parse] when given a LNURL-pay endpoint. -/// -/// It represents the endpoint's parameters for the LNURL workflow. -/// -/// See class LnUrlPayRequestData { final String callback; - - /// The minimum amount, in millisats, that this LNURL-pay endpoint accepts final int minSendable; - - /// The maximum amount, in millisats, that this LNURL-pay endpoint accepts final int maxSendable; - - /// As per LUD-06, `metadata` is a raw string (e.g. a json representation of the inner map). - /// Use `metadata_vec()` to get the parsed items. final String metadataStr; - - /// The comment length accepted by this endpoint - /// - /// See final int commentAllowed; - - /// Indicates the domain of the LNURL-pay service, to be shown to the user when asking for - /// payment input, as per LUD-06 spec. - /// - /// Note: this is not the domain of the callback, but the domain of the LNURL-pay endpoint. final String domain; - - /// Value indicating whether the recipient supports Nostr Zaps through NIP-57. - /// - /// See final bool allowsNostr; - - /// Optional recipient's lnurl provider's Nostr pubkey for NIP-57. If it exists it should be a - /// valid BIP 340 public key in hex. - /// - /// See - /// See final String? nostrPubkey; - - /// If sending to a LN Address, this will be filled. final String? lnAddress; const LnUrlPayRequestData({ @@ -999,17 +903,8 @@ class LnUrlPaySuccessData { } class LnUrlWithdrawRequest { - /// Request data containing information on how to call the lnurl withdraw - /// endpoint. Typically retrieved by calling `parse()` on a lnurl withdraw - /// input. final LnUrlWithdrawRequestData data; - - /// The amount to withdraw from the lnurl withdraw endpoint. Must be between - /// `min_withdrawable` and `max_withdrawable`. final int amountMsat; - - /// Optional description that will be put in the payment request for the - /// lnurl withdraw endpoint. final String? description; const LnUrlWithdrawRequest({ @@ -1019,20 +914,11 @@ class LnUrlWithdrawRequest { }); } -/// Wrapped in a [LnUrlWithdraw], this is the result of [parse] when given a LNURL-withdraw endpoint. -/// -/// It represents the endpoint's parameters for the LNURL workflow. -/// -/// See class LnUrlWithdrawRequestData { final String callback; final String k1; final String defaultDescription; - - /// The minimum amount, in millisats, that this LNURL-withdraw endpoint accepts final int minWithdrawable; - - /// The maximum amount, in millisats, that this LNURL-withdraw endpoint accepts final int maxWithdrawable; const LnUrlWithdrawRequestData({ @@ -1174,9 +1060,7 @@ class MetadataFilter { }); } -/// The different supported bitcoin networks enum Network { - /// Mainnet Bitcoin, Testnet, Signet, @@ -1739,7 +1623,6 @@ enum ReverseSwapStatus { CompletedConfirmed, } -/// A route hint for a LN payment class RouteHint { final List hops; @@ -1748,25 +1631,13 @@ class RouteHint { }); } -/// Details of a specific hop in a larger route hint class RouteHintHop { - /// The node_id of the non-target end of the route final String srcNodeId; - - /// The short_channel_id of this channel final int shortChannelId; - - /// The fees which must be paid to use this channel final int feesBaseMsat; final int feesProportionalMillionths; - - /// The difference in CLTV values between this node and the next node. final int cltvExpiryDelta; - - /// The minimum value, in msat, which must be relayed to the next hop. final int? htlcMinimumMsat; - - /// The maximum value in msat available for routing with a single HTLC. final int? htlcMaximumMsat; const RouteHintHop({ @@ -1897,19 +1768,12 @@ class StaticBackupResponse { @freezed sealed class SuccessActionProcessed with _$SuccessActionProcessed { - /// See [SuccessAction::Aes] for received payload - /// - /// See [AesSuccessActionDataDecrypted] for decrypted payload const factory SuccessActionProcessed.aes({ required AesSuccessActionDataResult result, }) = SuccessActionProcessed_Aes; - - /// See [SuccessAction::Message] const factory SuccessActionProcessed.message({ required MessageSuccessActionData data, }) = SuccessActionProcessed_Message; - - /// See [SuccessAction::Url] const factory SuccessActionProcessed.url({ required UrlSuccessActionData data, }) = SuccessActionProcessed_Url; @@ -6921,3 +6785,7 @@ const int INVOICE_PAYMENT_FEE_EXPIRY_SECONDS = 3600; const int ESTIMATED_CLAIM_TX_VSIZE = 138; const int ESTIMATED_LOCKUP_TX_VSIZE = 153; + +const int MOCK_REVERSE_SWAP_MIN = 50000; + +const int MOCK_REVERSE_SWAP_MAX = 1000000; diff --git a/tools/sdk-cli/Cargo.lock b/tools/sdk-cli/Cargo.lock index 20bb9d6ae..8f39c1163 100644 --- a/tools/sdk-cli/Cargo.lock +++ b/tools/sdk-cli/Cargo.lock @@ -485,8 +485,6 @@ dependencies = [ "aes", "anyhow", "base64 0.13.1", - "bip21", - "cbc", "chrono", "const_format", "ecies", @@ -501,13 +499,13 @@ dependencies = [ "once_cell", "openssl", "prost", - "querystring", "rand", "regex", "reqwest", "ripemd", "rusqlite", "rusqlite_migration", + "sdk-common", "serde", "serde_json", "serde_with", @@ -2668,6 +2666,30 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "sdk-common" +version = "0.4.1" +dependencies = [ + "aes", + "anyhow", + "base64 0.13.1", + "bip21", + "bitcoin 0.29.2", + "cbc", + "hex", + "lightning", + "lightning-invoice", + "log", + "querystring", + "regex", + "reqwest", + "rusqlite", + "serde", + "serde_json", + "strum_macros", + "thiserror", +] + [[package]] name = "secp256k1" version = "0.24.3"