Repository: breez/breez-sdk-greenlight (language: python)

Issue:
fix mirroring definitions
This puts the mirroring definitions in their right place and move out the test_utils to its own module.
There were two issues:
1. The mirroring required explicit use decleration, e.g: `pub use external_lib::{A,B,C ...};`
2. The flutter rust bridge didn't like the #[cfg(test)] on the test_utils so I used a folder with submodule for that.



Discussion / hints:

Fix patch:
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<string> 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<tokio::runtime::Runtime> = Lazy::new(|| tokio::runtime::Runtime::new().unwrap());
 static LOG_INIT: OnceCell<bool> = 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<InputType> {
         }
 
         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<InputType> {
                     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<InputType> {
     Err(anyhow!("Unrecognized input type"))
 }
 
-pub(crate) async fn post_and_log_response(url: &str, body: Option<String>) -> SdkResult<String> {
-    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<T>(
-    url: &str,
-    enforce_status_check: bool,
-) -> SdkResult<T>
-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::<T>(&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<String>)>
             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<String>)>
             }
 
             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> {
-    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<LnUrlRequestData> 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 <https://github.com/lnurl/luds/blob/luds/03.md>
-#[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 <https://github.com/lnurl/luds/blob/luds/04.md>
-#[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<String>,
-
-    /// 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<u64>,
     pub label: Option<String>,
     pub message: Option<String>,
@@ -724,23 +593,28 @@ impl From<Uri<'_>> 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<Mutex<ServerGuard>> =
-        Lazy::new(|| Mutex::new(Server::new()));
+    pub(crate) static MOCK_HTTP_SERVER: Lazy<Mutex<Server>> = 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<dyn std::error::Error>> {
@@ -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<T, E = InvoiceError> = Result<T, E>;
 
@@ -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<crate::lightning_invoice::CreationError> for InvoiceError {
-    fn from(err: crate::lightning_invoice::CreationError) -> Self {
+impl From<CreationError> for InvoiceError {
+    fn from(err: CreationError) -> Self {
         Self::Generic(err.to_string())
     }
 }
 
-impl From<crate::lightning_invoice::Bolt11ParseError> for InvoiceError {
-    fn from(err: crate::lightning_invoice::Bolt11ParseError) -> Self {
+impl From<Bolt11ParseError> for InvoiceError {
+    fn from(err: Bolt11ParseError) -> Self {
         Self::Validation(err.to_string())
     }
 }
 
-impl From<crate::lightning_invoice::Bolt11SemanticError> for InvoiceError {
-    fn from(err: crate::lightning_invoice::Bolt11SemanticError) -> Self {
+impl From<Bolt11SemanticError> 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<T, E = LnUrlError> = Result<T, E>;
 
@@ -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<FromUtf8Error> for LnUrlError {
     }
 }
 
-impl From<NodeError> 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<secp256k1::Error> for LnUrlError {
     fn from(err: secp256k1::Error) -> Self {
         Self::Generic(err.to_string())
@@ -93,3 +84,9 @@ impl From<TryFromSliceError> for LnUrlError {
         Self::Generic(err.to_string())
     }
 }
+
+impl From<InvoiceError> 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<String> {
+    // 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<String> {
+    // 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 <https://github.com/lnurl/luds/blob/luds/03.md> & LUD-04: <https://github.com/lnurl/luds/blob/luds/04.md>
+#[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
 /// <https://github.com/lnurl/luds/blob/luds/04.md>
 ///
+/// 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<dyn NodeAPI>,
+pub async fn perform_lnurl_auth(
+    linking_keys: KeyPair,
     req_data: LnUrlAuthRequestData,
 ) -> LnUrlResult<LnUrlCallbackStatus> {
-    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<LnUrlAuthRequestData> {
@@ -91,31 +87,24 @@ fn hmac_sha256(key: &[u8], input: &[u8]) -> Hmac<sha256::Hash> {
     Hmac::<sha256::Hash>::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<dyn NodeAPI>, url: Url) -> LnUrlResult<KeyPair> {
+pub fn get_derivation_path(
+    hashing_key: ExtendedPrivKey,
+    url: Url,
+) -> LnUrlResult<Vec<ChildNumber>> {
     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'/<long1>/<long2>/<long3>/<long4>
-    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 <https://github.com/lnurl/luds/blob/luds/04.md>
+    #[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<String>,
+
+        /// 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<aes::Aes256>;
+pub type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
+
+/// Validates invoice and performs the second and last step of LNURL-pay, as per
+/// <https://github.com/lnurl/luds/blob/luds/06.md>
+///
+/// See the [parse] docs for more detail on the full workflow.
+pub async fn validate_lnurl_pay(
+    user_amount_msat: u64,
+    comment: &Option<String>,
+    req_data: &LnUrlPayRequestData,
+    network: Network,
+) -> LnUrlResult<ValidatedCallbackResponse> {
+    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::<LnUrlErrorData>(&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<String>,
+    data: &LnUrlPayRequestData,
+) -> LnUrlResult<String> {
+    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<String>,
+    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<String>,
+        /// The external label or identifier of the [Payment]
+        pub payment_label: Option<String>,
+    }
+
+    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<SuccessActionProcessed>,
+    }
+
+    #[derive(Deserialize, Debug)]
+    #[serde(rename_all = "camelCase")]
+    pub struct CallbackResponse {
+        pub pr: String,
+        pub success_action: Option<SuccessAction>,
+    }
+
+    /// 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<Self> {
+            serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType)
+        }
+    }
+
+    impl ToSql for SuccessActionProcessed {
+        fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
+            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<String> {
+            let plaintext_bytes =
+                Aes256CbcDec::new_from_slices(key, &base64::decode(&self.iv)?)?
+                    .decrypt_padded_vec_mut::<Pkcs7>(&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<String> {
+            let ciphertext_bytes = Aes256CbcEnc::new_from_slices(key, iv)?
+                .encrypt_padded_vec_mut::<Pkcs7>(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<Self, Self::Error> {
+            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<anyhow::Error> for LnUrlPayError {
+        fn from(err: anyhow::Error) -> Self {
+            Self::Generic {
+                err: err.to_string(),
+            }
+        }
+    }
+
+    impl From<bitcoin::hashes::hex::Error> for LnUrlPayError {
+        fn from(err: bitcoin::hashes::hex::Error) -> Self {
+            Self::Generic {
+                err: err.to_string(),
+            }
+        }
+    }
+
+    impl From<InvoiceError> 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<LnUrlError> 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::<Pkcs7>(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::<Pkcs7>(&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
 /// <https://github.com/lnurl/luds/blob/luds/03.md>
@@ -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<LnUrlWithdrawResult> {
@@ -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<String> {
@@ -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<String>,
+    }
+
+    /// 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 <https://github.com/lnurl/luds/blob/luds/03.md>
+    #[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<InvoiceError> for LnUrlWithdrawError {
+        fn from(value: InvoiceError) -> Self {
+            match value {
+                InvoiceError::Validation(err) => Self::InvalidInvoice { err },
+                _ => Self::Generic {
+                    err: value.to_string(),
+                },
+            }
+        }
+    }
+
+    impl From<LnUrlError> 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<bitcoin::network::constants::Network> 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<Network> 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, ServiceConnectivityError> {
+    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<String>,
+) -> Result<String, ServiceConnectivityError> {
+    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<T>(
+    url: &str,
+    enforce_status_check: bool,
+) -> Result<T, ServiceConnectivityError>
+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::<T>(&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 <https://cjycode.com/flutter_rust_bridge/v1/feature/lang_external.html#types-in-other-crates>
+// 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<String>,
+    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<String>,
+    pub description_hash: Option<String>,
+    pub amount_msat: Option<u64>,
+    pub timestamp: u64,
+    pub expiry: u64,
+    pub routing_hints: Vec<RouteHint>,
+    pub payment_secret: Vec<u8>,
+    pub min_final_cltv_expiry_delta: u64,
+}
+
+#[frb(mirror(RouteHint))]
+pub struct _RouteHint {
+    pub hops: Vec<RouteHintHop>,
+}
+
+#[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<u64>,
+    pub htlc_maximum_msat: Option<u64>,
+}
+
+#[frb(mirror(LnUrlPayRequest))]
+pub struct _LnUrlPayRequest {
+    pub data: LnUrlPayRequestData,
+    pub amount_msat: u64,
+    pub comment: Option<String>,
+    pub payment_label: Option<String>,
+}
+
+#[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<String>,
+    pub ln_address: Option<String>,
+}
+
+#[frb(mirror(LnUrlWithdrawRequest))]
+pub struct _LnUrlWithdrawRequest {
+    pub data: LnUrlWithdrawRequestData,
+    pub amount_msat: u64,
+    pub description: Option<String>,
+}
+
+#[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<u64>,
+    pub label: Option<String>,
+    pub message: Option<String>,
+}
+
+#[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<Mutex<Option<...>>> 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<LNInvoice> {
 }
 
 pub fn parse_input(input: String) -> Result<InputType> {
-    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<ReceivePaymentRespo
 /*  LNURL API's */
 
 /// See [BreezServices::lnurl_pay]
-pub fn lnurl_pay(req: LnUrlPayRequest) -> Result<LnUrlPayResult> {
+pub fn lnurl_pay(req: LnUrlPayRequest) -> Result<crate::lnurl::pay::LnUrlPayResult> {
     block_on(async { get_breez_services().await?.lnurl_pay(req).await })
-        .map_err(anyhow::Error::new::<LnUrlPayError>)
+        .map_err(anyhow::Error::new::<crate::LnUrlPayError>)
 }
 
 /// See [BreezServices::lnurl_withdraw]
 pub fn lnurl_withdraw(req: LnUrlWithdrawRequest) -> Result<LnUrlWithdrawResult> {
     block_on(async { get_breez_services().await?.lnurl_withdraw(req).await })
-        .map_err(anyhow::Error::new::<LnUrlWithdrawError>)
+        .map_err(anyhow::Error::new::<crate::LnUrlWithdrawError>)
 }
 
 /// See [BreezServices::lnurl_auth]
-pub fn lnurl_auth(req_data: LnUrlAuthRequestData) -> Result<LnUrlCallbackStatus> {
+pub fn lnurl_auth(req_data: crate::LnUrlAuthRequestData) -> Result<LnUrlCallbackStatus> {
     block_on(async { get_breez_services().await?.lnurl_auth(req_data).await })
         .map_err(anyhow::Error::new::<LnUrlAuthError>)
 }
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<T, E = ConnectError> = Result<T, E>;
@@ -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<LnUrlCallbackStatus, LnUrlAuthError> {
-        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::<LnUrlError>::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<String> + 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<String> +
     )
 }
 fn wire_parse_input_impl(port_: MessagePort, input: impl Wire2Api<String> + 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<LnUrlWithdrawRequest> + 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<LnUrlAuthRequestData> + 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::<AesSuccessActionDataDecrypted>.unwrap();
+        let _: String = AesSuccessActionDataDecrypted.description;
+        let _: String = AesSuccessActionDataDecrypted.plaintext;
+    }
+    match None::<AesSuccessActionDataResult>.unwrap() {
+        AesSuccessActionDataResult::Decrypted { data } => {
+            let _: AesSuccessActionDataDecrypted = data;
+        }
+        AesSuccessActionDataResult::ErrorStatus { reason } => {
+            let _: String = reason;
+        }
+    }
+    {
+        let BitcoinAddressData = None::<BitcoinAddressData>.unwrap();
+        let _: String = BitcoinAddressData.address;
+        let _: Network = BitcoinAddressData.network;
+        let _: Option<u64> = BitcoinAddressData.amount_sat;
+        let _: Option<String> = BitcoinAddressData.label;
+        let _: Option<String> = BitcoinAddressData.message;
+    }
+    match None::<InputType>.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::<LNInvoice>.unwrap();
+        let _: String = LNInvoice.bolt11;
+        let _: Network = LNInvoice.network;
+        let _: String = LNInvoice.payee_pubkey;
+        let _: String = LNInvoice.payment_hash;
+        let _: Option<String> = LNInvoice.description;
+        let _: Option<String> = LNInvoice.description_hash;
+        let _: Option<u64> = LNInvoice.amount_msat;
+        let _: u64 = LNInvoice.timestamp;
+        let _: u64 = LNInvoice.expiry;
+        let _: Vec<RouteHint> = LNInvoice.routing_hints;
+        let _: Vec<u8> = LNInvoice.payment_secret;
+        let _: u64 = LNInvoice.min_final_cltv_expiry_delta;
+    }
+    {
+        let LnUrlAuthRequestData = None::<LnUrlAuthRequestData>.unwrap();
+        let _: String = LnUrlAuthRequestData.k1;
+        let _: Option<String> = LnUrlAuthRequestData.action;
+        let _: String = LnUrlAuthRequestData.domain;
+        let _: String = LnUrlAuthRequestData.url;
+    }
+    match None::<LnUrlCallbackStatus>.unwrap() {
+        LnUrlCallbackStatus::Ok => {}
+        LnUrlCallbackStatus::ErrorStatus { data } => {
+            let _: LnUrlErrorData = data;
+        }
+    }
+    {
+        let LnUrlErrorData = None::<LnUrlErrorData>.unwrap();
+        let _: String = LnUrlErrorData.reason;
+    }
+    {
+        let LnUrlPayErrorData = None::<LnUrlPayErrorData>.unwrap();
+        let _: String = LnUrlPayErrorData.payment_hash;
+        let _: String = LnUrlPayErrorData.reason;
+    }
+    {
+        let LnUrlPayRequestData = None::<LnUrlPayRequestData>.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<String> = LnUrlPayRequestData.nostr_pubkey;
+        let _: Option<String> = LnUrlPayRequestData.ln_address;
+    }
+    {
+        let LnUrlWithdrawRequestData = None::<LnUrlWithdrawRequestData>.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::<LnUrlWithdrawResult>.unwrap() {
+        LnUrlWithdrawResult::Ok { data } => {
+            let _: LnUrlWithdrawSuccessData = data;
+        }
+        LnUrlWithdrawResult::ErrorStatus { data } => {
+            let _: LnUrlErrorData = data;
+        }
+    }
+    {
+        let LnUrlWithdrawSuccessData = None::<LnUrlWithdrawSuccessData>.unwrap();
+        let _: LNInvoice = LnUrlWithdrawSuccessData.invoice;
+    }
+    {
+        let MessageSuccessActionData = None::<MessageSuccessActionData>.unwrap();
+        let _: String = MessageSuccessActionData.message;
+    }
+    match None::<Network>.unwrap() {
+        Network::Bitcoin => {}
+        Network::Testnet => {}
+        Network::Signet => {}
+        Network::Regtest => {}
+    }
+    {
+        let RouteHint = None::<RouteHint>.unwrap();
+        let _: Vec<RouteHintHop> = RouteHint.hops;
+    }
+    {
+        let RouteHintHop = None::<RouteHintHop>.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<u64> = RouteHintHop.htlc_minimum_msat;
+        let _: Option<u64> = RouteHintHop.htlc_maximum_msat;
+    }
+    match None::<SuccessActionProcessed>.unwrap() {
+        SuccessActionProcessed::Aes { result } => {
+            let _: AesSuccessActionDataResult = result;
+        }
+        SuccessActionProcessed::Message { data } => {
+            let _: MessageSuccessActionData = data;
+        }
+        SuccessActionProcessed::Url { data } => {
+            let _: UrlSuccessActionData = data;
+        }
+    }
+    {
+        let UrlSuccessActionData = None::<UrlSuccessActionData>.unwrap();
+        let _: String = UrlSuccessActionData.description;
+        let _: String = UrlSuccessActionData.url;
+    }
+};
 // Section: allocate functions
 
 // Section: related functions
@@ -1033,37 +1227,41 @@ impl Wire2Api<u8> 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<AesSuccessActionDataDecrypted> for AesSuccessActionDataDecrypted {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_AesSuccessActionDataDecrypted {}
+impl rust2dart::IntoIntoDart<mirror_AesSuccessActionDataDecrypted>
+    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<AesSuccessActionDataResult> for AesSuccessActionDataResult {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_AesSuccessActionDataResult {}
+impl rust2dart::IntoIntoDart<mirror_AesSuccessActionDataResult> for AesSuccessActionDataResult {
+    fn into_into_dart(self) -> mirror_AesSuccessActionDataResult {
+        mirror_AesSuccessActionDataResult(self)
     }
 }
 
@@ -1095,22 +1293,22 @@ impl rust2dart::IntoIntoDart<BackupStatus> 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<BitcoinAddressData> for BitcoinAddressData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_BitcoinAddressData {}
+impl rust2dart::IntoIntoDart<mirror_BitcoinAddressData> for BitcoinAddressData {
+    fn into_into_dart(self) -> mirror_BitcoinAddressData {
+        mirror_BitcoinAddressData(self)
     }
 }
 
@@ -1334,27 +1532,35 @@ impl rust2dart::IntoIntoDart<HealthCheckStatus> 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<InputType> for InputType {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_InputType {}
+impl rust2dart::IntoIntoDart<mirror_InputType> for InputType {
+    fn into_into_dart(self) -> mirror_InputType {
+        mirror_InputType(self)
     }
 }
 
@@ -1375,31 +1581,32 @@ impl rust2dart::IntoIntoDart<InvoicePaidDetails> 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<LNInvoice> for LNInvoice {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LNInvoice {}
+impl rust2dart::IntoIntoDart<mirror_LNInvoice> 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<LnPaymentDetails> 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<LnUrlAuthRequestData> for LnUrlAuthRequestData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlAuthRequestData {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlAuthRequestData> 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<LnUrlCallbackStatus> for LnUrlCallbackStatus {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlCallbackStatus {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlCallbackStatus> 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<LnUrlErrorData> for LnUrlErrorData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlErrorData {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlErrorData> 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<LnUrlPayErrorData> for LnUrlPayErrorData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlPayErrorData {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlPayErrorData> 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<LnUrlPayRequestData> for LnUrlPayRequestData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlPayRequestData {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlPayRequestData> 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<LnUrlPaySuccessData> 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<LnUrlWithdrawRequestData> for LnUrlWithdrawRequestData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlWithdrawRequestData {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlWithdrawRequestData> 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<LnUrlWithdrawResult> for LnUrlWithdrawResult {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlWithdrawResult {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlWithdrawResult> 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<LnUrlWithdrawSuccessData> for LnUrlWithdrawSuccessData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_LnUrlWithdrawSuccessData {}
+impl rust2dart::IntoIntoDart<mirror_LnUrlWithdrawSuccessData> for LnUrlWithdrawSuccessData {
+    fn into_into_dart(self) -> mirror_LnUrlWithdrawSuccessData {
+        mirror_LnUrlWithdrawSuccessData(self)
     }
 }
 
@@ -1686,33 +1903,33 @@ impl rust2dart::IntoIntoDart<MaxReverseSwapAmountResponse> 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<MessageSuccessActionData> for MessageSuccessActionData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_MessageSuccessActionData {}
+impl rust2dart::IntoIntoDart<mirror_MessageSuccessActionData> 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<Network> for Network {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_Network {}
+impl rust2dart::IntoIntoDart<mirror_Network> 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<ReverseSwapStatus> 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<RouteHint> for RouteHint {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_RouteHint {}
+impl rust2dart::IntoIntoDart<mirror_RouteHint> 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<RouteHintHop> for RouteHintHop {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_RouteHintHop {}
+impl rust2dart::IntoIntoDart<mirror_RouteHintHop> for RouteHintHop {
+    fn into_into_dart(self) -> mirror_RouteHintHop {
+        mirror_RouteHintHop(self)
     }
 }
 
@@ -2232,20 +2450,26 @@ impl rust2dart::IntoIntoDart<StaticBackupResponse> 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<SuccessActionProcessed> for SuccessActionProcessed {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_SuccessActionProcessed {}
+impl rust2dart::IntoIntoDart<mirror_SuccessActionProcessed> for SuccessActionProcessed {
+    fn into_into_dart(self) -> mirror_SuccessActionProcessed {
+        mirror_SuccessActionProcessed(self)
     }
 }
 
@@ -2344,19 +2568,19 @@ impl rust2dart::IntoIntoDart<UnspentTransactionOutput> 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<UrlSuccessActionData> for UrlSuccessActionData {
-    fn into_into_dart(self) -> Self {
-        self
+impl support::IntoDartExceptPrimitive for mirror_UrlSuccessActionData {}
+impl rust2dart::IntoIntoDart<mirror_UrlSuccessActionData> 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<RecommendedFees> {
-        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<Vec<OnchainTx>> {
-        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<u32> {
-        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<Vec<Outspend>> {
         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<u8>) -> SdkResult<String> {
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<T, E = SdkError> = Result<T, E>;
@@ -103,100 +104,6 @@ impl From<SdkError> 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<anyhow::Error> for LnUrlPayError {
-    fn from(err: anyhow::Error) -> Self {
-        Self::Generic {
-            err: err.to_string(),
-        }
-    }
-}
-
-impl From<crate::bitcoin::hashes::hex::Error> for LnUrlPayError {
-    fn from(err: crate::bitcoin::hashes::hex::Error) -> Self {
-        Self::Generic {
-            err: err.to_string(),
-        }
-    }
-}
-
-impl From<InvoiceError> 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<LnUrlError> 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<PersistError> for LnUrlPayError {
     fn from(err: PersistError) -> Self {
         Self::Generic {
@@ -232,59 +139,6 @@ impl From<SendPaymentError> 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<InvoiceError> for LnUrlWithdrawError {
-    fn from(value: InvoiceError) -> Self {
-        match value {
-            InvoiceError::Validation(err) => Self::InvalidInvoice { err },
-            _ => Self::Generic {
-                err: value.to_string(),
-            },
-        }
-    }
-}
-
-impl From<LnUrlError> 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<PersistError> for LnUrlWithdrawError {
     fn from(err: PersistError) -> Self {
         Self::Generic {
@@ -495,6 +349,12 @@ impl SdkError {
     }
 }
 
+impl From<ServiceConnectivityError> for SdkError {
+    fn from(value: ServiceConnectivityError) -> Self {
+        Self::ServiceConnectivity { err: value.err }
+    }
+}
+
 impl From<anyhow::Error> for SdkError {
     fn from(err: anyhow::Error) -> Self {
         Self::Generic {
@@ -794,12 +654,3 @@ impl From<SystemTimeError> 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 dfc5f794f..b49da34af 100644
--- a/libs/sdk-core/src/greenlight/node_api.rs
+++ b/libs/sdk-core/src/greenlight/node_api.rs
@@ -26,6 +26,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};
@@ -44,7 +45,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<String> {
-    // 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<String> {
-    // When not called from a test, we fallback to keeping the URL intact
-    Ok(lnurl_endpoint)
+    pub(crate) static MOCK_HTTP_SERVER: Lazy<Mutex<Server>> = 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<aes::Aes256>;
-type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
-
-/// Validates invoice and performs the second and last step of LNURL-pay, as per
-/// <https://github.com/lnurl/luds/blob/luds/06.md>
+/// 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<String>,
-    req_data: &LnUrlPayRequestData,
-    network: Network,
-) -> LnUrlResult<ValidatedCallbackResponse> {
-    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::<LnUrlErrorData>(&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<String>,
-    data: &LnUrlPayRequestData,
-) -> LnUrlResult<String> {
-    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<String>,
-    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
+/// <https://github.com/lnurl/luds/blob/luds/09.md>
+///
+/// * `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
-    /// <https://github.com/lnurl/luds/blob/luds/09.md>
-    ///
-    /// * `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<SuccessActionProcessed>,
-    }
-
-    #[derive(Deserialize, Debug)]
-    #[serde(rename_all = "camelCase")]
-    pub struct CallbackResponse {
-        pub pr: String,
-        pub success_action: Option<SuccessAction>,
-    }
-
-    /// 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<String> {
-            let plaintext_bytes =
-                Aes256CbcDec::new_from_slices(key, &base64::decode(&self.iv)?)?
-                    .decrypt_padded_vec_mut::<Pkcs7>(&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<String> {
-            let ciphertext_bytes = Aes256CbcEnc::new_from_slices(key, iv)?
-                .encrypt_padded_vec_mut::<Pkcs7>(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<Self, Self::Error> {
-            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<SuccessActionProcessed>,
 }
 
 #[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::<Pkcs7>(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::<Pkcs7>(&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<u8>,
 }
 
-/// The different supported bitcoin networks
-#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, Serialize, Deserialize)]
-pub enum Network {
-    /// Mainnet
-    Bitcoin,
-    Testnet,
-    Signet,
-    Regtest,
-}
-
-impl From<crate::bitcoin::network::constants::Network> 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<Network> 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 <https://github.com/lnurl/luds/blob/luds/03.md> & LUD-04: <https://github.com/lnurl/luds/blob/luds/04.md>
-#[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<String>,
-}
-
-/// 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<String>,
-    /// The external label or identifier of the [Payment]
-    pub payment_label: Option<String>,
-}
-
-/// [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<T, E = NodeError> = Result<T, E>;
 
@@ -69,6 +71,27 @@ impl NodeError {
     }
 }
 
+impl From<NodeError> 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<NodeError> 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<Self> {
-        serde_json::from_str(value.as_str()?).map_err(|_| FromSqlError::InvalidType)
-    }
-}
-
-impl ToSql for SuccessActionProcessed {
-    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
-        Ok(ToSqlOutput::from(
-            serde_json::to_string(&self).map_err(|_| FromSqlError::InvalidType)?,
-        ))
-    }
-}
-
 #[test]
 fn test_ln_transactions() -> PersistResult<(), Box<dyn std::error::Error>> {
-    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<BoltzApiCreateReverseSwapResponse> {
-        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<BoltzApiReverseSwapStatus> {
-        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<Vec<RouteHint>> {
-        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<ReverseSwapPairInfo> {
-    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-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 <https://github.com/lnurl/luds/blob/luds/04.md>
 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 <https://github.com/lnurl/luds/blob/luds/06.md>
 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 <https://github.com/lnurl/luds/blob/luds/12.md>
   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 <https://github.com/nostr-protocol/nips/blob/master/57.md>
   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 <https://github.com/nostr-protocol/nips/blob/master/57.md>
-  /// See <https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki>
   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 <https://github.com/lnurl/luds/blob/luds/03.md>
 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<RouteHintHop> 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"


Test patch:
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<OnchainTx> = 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,