diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index cc585c3fe..c0a58f1e7 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -192,6 +192,7 @@ dictionary LnPaymentDetails { SuccessActionProcessed? lnurl_success_action; string? lnurl_metadata; string? ln_address; + string? lnurl_withdraw_endpoint; }; dictionary ClosedChannelPaymentDetails { @@ -473,6 +474,16 @@ interface LnUrlCallbackStatus { ErrorStatus(LnUrlErrorData data); }; +[Enum] +interface LnUrlWithdrawResult { + Ok(LnUrlWithdrawSuccessData data); + ErrorStatus(LnUrlErrorData data); +}; + +dictionary LnUrlWithdrawSuccessData { + LNInvoice invoice; +}; + dictionary LnUrlAuthRequestData { string k1; string? action; @@ -527,7 +538,7 @@ interface BlockingBreezServices { LnUrlPayResult pay_lnurl(LnUrlPayRequestData req_data, u64 amount_sats, string? comment); [Throws=SdkError] - LnUrlCallbackStatus withdraw_lnurl(LnUrlWithdrawRequestData req_data, u64 amount_sats, string? description); + LnUrlWithdrawResult withdraw_lnurl(LnUrlWithdrawRequestData req_data, u64 amount_sats, string? description); [Throws=SdkError] LnUrlCallbackStatus lnurl_auth(LnUrlAuthRequestData req_data); diff --git a/libs/sdk-bindings/src/uniffi_binding.rs b/libs/sdk-bindings/src/uniffi_binding.rs index 99a8254e1..a4d26f8f5 100644 --- a/libs/sdk-bindings/src/uniffi_binding.rs +++ b/libs/sdk-bindings/src/uniffi_binding.rs @@ -13,10 +13,11 @@ use breez_sdk_core::{ FeeratePreset, FiatCurrency, GreenlightCredentials, GreenlightNodeConfig, InputType, InvoicePaidDetails, LNInvoice, ListPaymentsRequest, LnPaymentDetails, LnUrlAuthRequestData, LnUrlCallbackStatus, LnUrlErrorData, LnUrlPayRequestData, LnUrlPayResult, - LnUrlWithdrawRequestData, LocaleOverrides, LocalizedName, LogEntry, LogStream, LspInformation, - MessageSuccessActionData, MetadataItem, Network, NodeConfig, NodeState, OpenChannelFeeRequest, - OpenChannelFeeResponse, OpeningFeeParams, OpeningFeeParamsMenu, Payment, PaymentDetails, - PaymentFailedData, PaymentStatus, PaymentType, PaymentTypeFilter, Rate, ReceiveOnchainRequest, + LnUrlWithdrawRequestData, LnUrlWithdrawResult, LnUrlWithdrawSuccessData, LocaleOverrides, + LocalizedName, LogEntry, LogStream, LspInformation, MessageSuccessActionData, MetadataItem, + Network, NodeConfig, NodeState, OpenChannelFeeRequest, OpenChannelFeeResponse, + OpeningFeeParams, OpeningFeeParamsMenu, Payment, PaymentDetails, PaymentFailedData, + PaymentStatus, PaymentType, PaymentTypeFilter, Rate, ReceiveOnchainRequest, ReceivePaymentRequest, ReceivePaymentResponse, RecommendedFees, ReverseSwapFeesRequest, ReverseSwapInfo, ReverseSwapPairInfo, ReverseSwapStatus, RouteHint, RouteHintHop, SignMessageRequest, SignMessageResponse, StaticBackupRequest, StaticBackupResponse, @@ -184,7 +185,7 @@ impl BlockingBreezServices { req_data: LnUrlWithdrawRequestData, amount_sats: u64, description: Option, - ) -> SdkResult { + ) -> SdkResult { rt().block_on( self.breez_services .lnurl_withdraw(req_data, amount_sats, description), diff --git a/libs/sdk-core/src/binding.rs b/libs/sdk-core/src/binding.rs index 3e4abcc4f..2100b8b92 100644 --- a/libs/sdk-core/src/binding.rs +++ b/libs/sdk-core/src/binding.rs @@ -32,10 +32,10 @@ use crate::lsp::LspInformation; use crate::models::{Config, LogEntry, NodeState, Payment, SwapInfo}; use crate::{ BackupStatus, BuyBitcoinRequest, BuyBitcoinResponse, CheckMessageRequest, CheckMessageResponse, - EnvironmentType, ListPaymentsRequest, LnUrlCallbackStatus, NodeConfig, OpenChannelFeeRequest, - OpenChannelFeeResponse, ReceiveOnchainRequest, ReceivePaymentRequest, ReceivePaymentResponse, - ReverseSwapFeesRequest, ReverseSwapInfo, ReverseSwapPairInfo, SignMessageRequest, - SignMessageResponse, StaticBackupRequest, StaticBackupResponse, + EnvironmentType, ListPaymentsRequest, LnUrlCallbackStatus, LnUrlWithdrawResult, NodeConfig, + OpenChannelFeeRequest, OpenChannelFeeResponse, ReceiveOnchainRequest, ReceivePaymentRequest, + ReceivePaymentResponse, ReverseSwapFeesRequest, ReverseSwapInfo, ReverseSwapPairInfo, + SignMessageRequest, SignMessageResponse, StaticBackupRequest, StaticBackupResponse, }; /* @@ -274,7 +274,7 @@ pub fn lnurl_withdraw( req_data: LnUrlWithdrawRequestData, amount_sats: u64, description: Option, -) -> Result { +) -> Result { block_on(async { get_breez_services() .await? diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 9f15ae688..0d60fb83d 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -330,6 +330,7 @@ impl BreezServices { maybe_sa_processed.as_ref(), Some(req_data.metadata_str), req_data.ln_address, + None, )?; Ok(LnUrlPayResult::EndpointSuccess { @@ -350,7 +351,7 @@ impl BreezServices { req_data: LnUrlWithdrawRequestData, amount_sats: u64, description: Option, - ) -> Result { + ) -> Result { let invoice = self .receive_payment(ReceivePaymentRequest { amount_sats, @@ -364,7 +365,22 @@ impl BreezServices { .await .map_err(|_| anyhow!("Failed to receive payment"))? .ln_invoice; - validate_lnurl_withdraw(req_data, invoice).await + + let lnurl_w_endpoint = req_data.callback.clone(); + let res = validate_lnurl_withdraw(req_data, invoice).await?; + + if let LnUrlWithdrawResult::Ok { ref data } = res { + // If endpoint was successfully called, store the LNURL-withdraw endpoint URL as metadata linked to the invoice + self.persister.insert_lnurl_payment_external_info( + &data.invoice.payment_hash, + None, + None, + None, + Some(lnurl_w_endpoint), + )?; + } + + Ok(res) } /// Third and last step of LNURL-auth. The first step is `parse()`, which also validates the LNURL destination @@ -1880,12 +1896,14 @@ pub(crate) mod tests { let lnurl_metadata = "{'key': 'sample-metadata-val'}"; let test_ln_address = "test@ln-address.com"; + let test_lnurl_withdraw_endpoint = "https://test.endpoint.lnurl-w"; let sa = SuccessActionProcessed::Message { data: MessageSuccessActionData { message: "test message".into(), }, }; + let payment_hash_lnurl_withdraw = "2222"; let payment_hash_with_lnurl_success_action = "3333"; let dummy_transactions = vec![ Payment { @@ -1907,6 +1925,30 @@ pub(crate) mod tests { lnurl_success_action: None, lnurl_metadata: None, ln_address: None, + lnurl_withdraw_endpoint: None, + }, + }, + }, + Payment { + id: payment_hash_lnurl_withdraw.to_string(), + payment_type: PaymentType::Received, + payment_time: 150000, + amount_msat: 10, + fee_msat: 0, + status: PaymentStatus::Complete, + description: Some("test lnurl-withdraw receive".to_string()), + details: PaymentDetails::Ln { + data: LnPaymentDetails { + payment_hash: payment_hash_lnurl_withdraw.to_string(), + label: "".to_string(), + destination_pubkey: "1111".to_string(), + payment_preimage: "2222".to_string(), + keysend: false, + bolt11: "1111".to_string(), + lnurl_success_action: None, + lnurl_metadata: None, + ln_address: None, + lnurl_withdraw_endpoint: Some(test_lnurl_withdraw_endpoint.to_string()), }, }, }, @@ -1929,6 +1971,7 @@ pub(crate) mod tests { lnurl_success_action: Some(sa.clone()), lnurl_metadata: Some(lnurl_metadata.to_string()), ln_address: Some(test_ln_address.to_string()), + lnurl_withdraw_endpoint: None, }, }, }, @@ -1944,6 +1987,14 @@ pub(crate) mod tests { Some(&sa), Some(lnurl_metadata.to_string()), Some(test_ln_address.to_string()), + None, + )?; + persister.insert_lnurl_payment_external_info( + payment_hash_lnurl_withdraw, + None, + None, + None, + Some(test_lnurl_withdraw_endpoint.to_string()), )?; let mut builder = BreezServicesBuilder::new(test_config.clone()); @@ -1982,7 +2033,7 @@ pub(crate) mod tests { include_failures: None, }) .await?; - assert_eq!(received, vec![cloned[0].clone()]); + assert_eq!(received, vec![cloned[1].clone(), cloned[0].clone()]); let sent = breez_services .list_payments(ListPaymentsRequest { @@ -1992,11 +2043,19 @@ pub(crate) mod tests { include_failures: None, }) .await?; - assert_eq!(sent, vec![cloned[1].clone()]); + assert_eq!(sent, vec![cloned[2].clone()]); + assert!(matches!( + &sent[0].details, + PaymentDetails::Ln {data: LnPaymentDetails {lnurl_success_action, ..}} + if lnurl_success_action == &Some(sa))); assert!(matches!( - &sent[0].details, PaymentDetails::Ln {data: LnPaymentDetails {lnurl_success_action, ..}} if lnurl_success_action == &Some(sa))); + &sent[0].details, + PaymentDetails::Ln {data: LnPaymentDetails {ln_address, ..}} + if ln_address == &Some(test_ln_address.to_string()))); assert!(matches!( - &sent[0].details, PaymentDetails::Ln {data: LnPaymentDetails {ln_address, ..}} if ln_address == &Some(test_ln_address.to_string()))); + &received[0].details, + PaymentDetails::Ln {data: LnPaymentDetails {lnurl_withdraw_endpoint, ..}} + if lnurl_withdraw_endpoint == &Some(test_lnurl_withdraw_endpoint.to_string()))); Ok(()) } diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index 084872322..5265bf076 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -62,6 +62,8 @@ use crate::models::GreenlightNodeConfig; use crate::models::ListPaymentsRequest; use crate::models::LnPaymentDetails; use crate::models::LnUrlCallbackStatus; +use crate::models::LnUrlWithdrawResult; +use crate::models::LnUrlWithdrawSuccessData; use crate::models::LogEntry; use crate::models::Network; use crate::models::NodeConfig; @@ -1019,6 +1021,7 @@ impl support::IntoDart for LnPaymentDetails { self.lnurl_success_action.into_dart(), self.ln_address.into_dart(), self.lnurl_metadata.into_dart(), + self.lnurl_withdraw_endpoint.into_dart(), ] .into_dart() } @@ -1095,6 +1098,23 @@ impl support::IntoDart for LnUrlWithdrawRequestData { } impl support::IntoDartExceptPrimitive for LnUrlWithdrawRequestData {} +impl support::IntoDart for LnUrlWithdrawResult { + fn into_dart(self) -> support::DartAbi { + match self { + Self::Ok { data } => vec![0.into_dart(), data.into_dart()], + Self::ErrorStatus { data } => vec![1.into_dart(), data.into_dart()], + } + .into_dart() + } +} +impl support::IntoDartExceptPrimitive for LnUrlWithdrawResult {} +impl support::IntoDart for LnUrlWithdrawSuccessData { + fn into_dart(self) -> support::DartAbi { + vec![self.invoice.into_dart()].into_dart() + } +} +impl support::IntoDartExceptPrimitive for LnUrlWithdrawSuccessData {} + impl support::IntoDart for LocaleOverrides { fn into_dart(self) -> support::DartAbi { vec![ diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index d130174ae..dce3c273f 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -901,6 +901,7 @@ impl TryFrom for Payment { lnurl_success_action: None, // For received payments, this is None lnurl_metadata: None, // For received payments, this is None ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }) @@ -935,6 +936,7 @@ impl TryFrom for Payment { lnurl_success_action: None, // For received payments, this is None lnurl_metadata: None, // For received payments, this is None ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }) @@ -984,6 +986,7 @@ impl TryFrom for Payment { lnurl_success_action: None, lnurl_metadata: None, ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }) @@ -1022,6 +1025,7 @@ impl TryFrom for Payment { lnurl_success_action: None, // For received payments, this is None lnurl_metadata: None, // For received payments, this is None ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }) @@ -1083,6 +1087,7 @@ impl TryFrom for Payment { lnurl_success_action: None, lnurl_metadata: None, ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }) diff --git a/libs/sdk-core/src/lnurl/withdraw.rs b/libs/sdk-core/src/lnurl/withdraw.rs index a789d8581..7e74c2aae 100644 --- a/libs/sdk-core/src/lnurl/withdraw.rs +++ b/libs/sdk-core/src/lnurl/withdraw.rs @@ -1,9 +1,9 @@ use std::str::FromStr; use crate::input_parser::get_parse_and_log_response; -use crate::{lnurl::*, LnUrlCallbackStatus}; +use crate::{lnurl::*, LnUrlCallbackStatus, LnUrlWithdrawResult, LnUrlWithdrawSuccessData}; use crate::{LNInvoice, LnUrlWithdrawRequestData}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, ensure, Result}; /// Validates invoice and performs the second and last step of LNURL-withdraw, as per /// @@ -16,23 +16,32 @@ use anyhow::{anyhow, Result}; pub(crate) async fn validate_lnurl_withdraw( req_data: LnUrlWithdrawRequestData, invoice: LNInvoice, -) -> Result { - match invoice +) -> Result { + let amount_msat = invoice .amount_msat .ok_or("Expected invoice amount, but found none") - .map_err(|e| anyhow!(e))? - { - n if n < req_data.min_withdrawable => Err(anyhow!( - "Amount is smaller than the minimum allowed by the LNURL-withdraw endpoint" - )), - n if n > req_data.max_withdrawable => Err(anyhow!( - "Amount is bigger than the maximum allowed by the LNURL-withdraw endpoint" - )), - _ => { - let callback_url = build_withdraw_callback_url(&req_data, &invoice)?; - get_parse_and_log_response(&callback_url).await - } - } + .map_err(|e| anyhow!(e))?; + + ensure!( + amount_msat >= req_data.min_withdrawable, + "Amount is smaller than the minimum allowed by the LNURL-withdraw endpoint" + ); + ensure!( + amount_msat <= req_data.max_withdrawable, + "Amount is bigger than the maximum allowed by the LNURL-withdraw endpoint" + ); + + // Send invoice to the LNURL-w endpoint via the callback + let callback_url = build_withdraw_callback_url(&req_data, &invoice)?; + let callback_res: LnUrlCallbackStatus = get_parse_and_log_response(&callback_url).await?; + let withdraw_status = match callback_res { + LnUrlCallbackStatus::Ok => LnUrlWithdrawResult::Ok { + data: LnUrlWithdrawSuccessData { invoice }, + }, + LnUrlCallbackStatus::ErrorStatus { data } => LnUrlWithdrawResult::ErrorStatus { data }, + }; + + Ok(withdraw_status) } fn build_withdraw_callback_url( @@ -101,14 +110,14 @@ mod tests { #[tokio::test] async fn test_lnurl_withdraw_success() -> Result<()> { let invoice_str = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz"; - let invoice = crate::invoice::parse_invoice(invoice_str)?; + let req_invoice = crate::invoice::parse_invoice(invoice_str)?; let withdraw_req = get_test_withdraw_req_data(0, 100); - let _m = mock_lnurl_withdraw_callback(&withdraw_req, &invoice, None)?; + let _m = mock_lnurl_withdraw_callback(&withdraw_req, &req_invoice, None)?; assert!(matches!( - validate_lnurl_withdraw(withdraw_req, invoice).await?, - LnUrlCallbackStatus::Ok + validate_lnurl_withdraw(withdraw_req, req_invoice.clone()).await?, + LnUrlWithdrawResult::Ok { data: LnUrlWithdrawSuccessData { invoice } } if invoice == req_invoice )); Ok(()) @@ -139,7 +148,7 @@ mod tests { assert!(matches!( validate_lnurl_withdraw(withdraw_req, invoice).await?, - LnUrlCallbackStatus::ErrorStatus { data: _ } + LnUrlWithdrawResult::ErrorStatus { data: _ } )); Ok(()) diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index d3eabdbee..a4ad63f83 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -698,6 +698,9 @@ pub struct LnPaymentDetails { /// Only set for [PaymentType::Sent] payments where the receiver endpoint returned LNURL metadata pub lnurl_metadata: Option, + + /// Only set for [PaymentType::Received] payments that were received as part of LNURL-withdraw + pub lnurl_withdraw_endpoint: Option, } /// Represents the funds that were on the user side of the channel at the time it was closed. @@ -1092,7 +1095,7 @@ pub struct UnspentTransactionOutput { pub reserved_to_block: u32, } -//// Contains the result of the entire LNURL interaction, as reported by the LNURL endpoint. +/// 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, @@ -1114,6 +1117,18 @@ pub enum LnUrlCallbackStatus { }, } +/// [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/persist/migrations.rs b/libs/sdk-core/src/persist/migrations.rs index 4b31e7c53..16626def2 100644 --- a/libs/sdk-core/src/persist/migrations.rs +++ b/libs/sdk-core/src/persist/migrations.rs @@ -439,6 +439,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { ALTER TABLE payments RENAME COLUMN pending TO status; UPDATE payments SET status = CASE WHEN status = 1 THEN 0 ELSE 1 END; ", + "ALTER TABLE sync.payments_external_info ADD COLUMN lnurl_withdraw_endpoint TEXT;", ] } diff --git a/libs/sdk-core/src/persist/sync.rs b/libs/sdk-core/src/persist/sync.rs index d735484a6..297118e07 100644 --- a/libs/sdk-core/src/persist/sync.rs +++ b/libs/sdk-core/src/persist/sync.rs @@ -163,7 +163,8 @@ impl SqliteStorage { payment_id, lnurl_success_action, ln_address, - lnurl_metadata + lnurl_metadata, + lnurl_withdraw_endpoint FROM remote_sync.payments_external_info WHERE payment_id NOT IN (SELECT payment_id FROM sync.payments_external_info);", [], diff --git a/libs/sdk-core/src/persist/transactions.rs b/libs/sdk-core/src/persist/transactions.rs index 27a6edcb5..acbc857b3 100644 --- a/libs/sdk-core/src/persist/transactions.rs +++ b/libs/sdk-core/src/persist/transactions.rs @@ -68,6 +68,7 @@ impl SqliteStorage { lnurl_pay_success_action: Option<&SuccessActionProcessed>, lnurl_metadata: Option, ln_address: Option, + lnurl_withdraw_endpoint: Option, ) -> SdkResult<()> { let con = self.get_connection()?; let mut prep_statement = con.prepare( @@ -76,9 +77,10 @@ impl SqliteStorage { payment_id, lnurl_success_action, lnurl_metadata, - ln_address + ln_address, + lnurl_withdraw_endpoint ) - VALUES (?1,?2,?3,?4) + VALUES (?1,?2,?3,?4,?5) ", )?; @@ -87,6 +89,7 @@ impl SqliteStorage { &lnurl_pay_success_action, lnurl_metadata, ln_address, + lnurl_withdraw_endpoint, ))?; Ok(()) @@ -151,6 +154,7 @@ impl SqliteStorage { e.lnurl_success_action, e.lnurl_metadata, e.ln_address, + e.lnurl_withdraw_endpoint, o.payer_amount_msat FROM payments p LEFT JOIN sync.payments_external_info e @@ -194,6 +198,7 @@ impl SqliteStorage { e.lnurl_success_action, e.lnurl_metadata, e.ln_address, + e.lnurl_withdraw_endpoint, o.payer_amount_msat FROM payments p LEFT JOIN sync.payments_external_info e @@ -239,10 +244,11 @@ impl SqliteStorage { data.lnurl_success_action = row.get(8)?; data.lnurl_metadata = row.get(9)?; data.ln_address = row.get(10)?; + data.lnurl_withdraw_endpoint = row.get(11)?; } // In case we have a record of the open channel fee, let's use it. - let payer_amount_msat: Option = row.get(11)?; + let payer_amount_msat: Option = row.get(12)?; if let Some(payer_amount) = payer_amount_msat { payment.fee_msat = payer_amount - amount_msat; } @@ -356,6 +362,8 @@ fn test_ln_transactions() -> Result<(), Box> { }; let payment_hash_with_lnurl_success_action = "123"; + let payment_hash_with_lnurl_withdraw = "124"; + let lnurl_withdraw_url = "https://test.lnurl.withdraw.link"; let txs = [ Payment { id: payment_hash_with_lnurl_success_action.to_string(), @@ -376,11 +384,12 @@ fn test_ln_transactions() -> Result<(), Box> { lnurl_success_action: Some(sa.clone()), lnurl_metadata: Some(lnurl_metadata.to_string()), ln_address: Some(test_ln_address.to_string()), + lnurl_withdraw_endpoint: None, }, }, }, Payment { - id: "124".to_string(), + id: payment_hash_with_lnurl_withdraw.to_string(), payment_type: PaymentType::Received, payment_time: 1000, amount_msat: 100, @@ -389,7 +398,7 @@ fn test_ln_transactions() -> Result<(), Box> { description: Some("desc".to_string()), details: PaymentDetails::Ln { data: LnPaymentDetails { - payment_hash: "124".to_string(), + payment_hash: payment_hash_with_lnurl_withdraw.to_string(), label: "label".to_string(), destination_pubkey: "pubey".to_string(), payment_preimage: "payment_preimage".to_string(), @@ -398,6 +407,7 @@ fn test_ln_transactions() -> Result<(), Box> { lnurl_success_action: None, lnurl_metadata: None, ln_address: None, + lnurl_withdraw_endpoint: Some(lnurl_withdraw_url.to_string()), }, }, }, @@ -421,6 +431,7 @@ fn test_ln_transactions() -> Result<(), Box> { lnurl_success_action: None, lnurl_metadata: None, ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }]; @@ -433,6 +444,14 @@ fn test_ln_transactions() -> Result<(), Box> { Some(&sa), Some(lnurl_metadata.to_string()), Some(test_ln_address.to_string()), + None, + )?; + storage.insert_lnurl_payment_external_info( + payment_hash_with_lnurl_withdraw, + None, + None, + None, + Some(lnurl_withdraw_url.to_string()), )?; // retrieve all diff --git a/libs/sdk-core/src/swap.rs b/libs/sdk-core/src/swap.rs index bed6bd0a2..f3dc2ec98 100644 --- a/libs/sdk-core/src/swap.rs +++ b/libs/sdk-core/src/swap.rs @@ -828,6 +828,7 @@ mod tests { lnurl_success_action: None, lnurl_metadata: None, ln_address: None, + lnurl_withdraw_endpoint: None, }, }, }; diff --git a/libs/sdk-flutter/lib/breez_bridge.dart b/libs/sdk-flutter/lib/breez_bridge.dart index fc9004949..a333108b8 100644 --- a/libs/sdk-flutter/lib/breez_bridge.dart +++ b/libs/sdk-flutter/lib/breez_bridge.dart @@ -289,7 +289,7 @@ class BreezSDK { /// This call will validate the given `amount_sats` against the parameters /// of the LNURL endpoint (`req_data`). If they match the endpoint requirements, the LNURL withdraw /// request is made. A successful result here means the endpoint started the payment. - Future lnurlWithdraw({ + Future lnurlWithdraw({ required int amountSats, String? description, required LnUrlWithdrawRequestData reqData, diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index cdc0e9324..75105828f 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -158,7 +158,7 @@ abstract class BreezSdkCore { FlutterRustBridgeTaskConstMeta get kLnurlPayConstMeta; /// See [BreezServices::lnurl_withdraw] - Future lnurlWithdraw( + Future lnurlWithdraw( {required LnUrlWithdrawRequestData reqData, required int amountSats, String? description, @@ -636,6 +636,9 @@ class LnPaymentDetails { /// Only set for [PaymentType::Sent] payments where the receiver endpoint returned LNURL metadata final String? lnurlMetadata; + /// Only set for [PaymentType::Received] payments that were received as part of LNURL-withdraw + final String? lnurlWithdrawEndpoint; + const LnPaymentDetails({ required this.paymentHash, required this.label, @@ -646,6 +649,7 @@ class LnPaymentDetails { this.lnurlSuccessAction, this.lnAddress, this.lnurlMetadata, + this.lnurlWithdrawEndpoint, }); } @@ -775,6 +779,24 @@ class LnUrlWithdrawRequestData { }); } +@freezed +class LnUrlWithdrawResult with _$LnUrlWithdrawResult { + const factory LnUrlWithdrawResult.ok({ + required LnUrlWithdrawSuccessData data, + }) = LnUrlWithdrawResult_Ok; + const factory LnUrlWithdrawResult.errorStatus({ + required LnUrlErrorData data, + }) = LnUrlWithdrawResult_ErrorStatus; +} + +class LnUrlWithdrawSuccessData { + final LNInvoice invoice; + + const LnUrlWithdrawSuccessData({ + required this.invoice, + }); +} + /// Locale-specific settings for the representation of a currency class LocaleOverrides { final String locale; @@ -1874,7 +1896,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { argNames: ["userAmountSat", "comment", "reqData"], ); - Future lnurlWithdraw( + Future lnurlWithdraw( {required LnUrlWithdrawRequestData reqData, required int amountSats, String? description, @@ -1884,7 +1906,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { var arg2 = _platform.api2wire_opt_String(description); return _platform.executeNormal(FlutterRustBridgeTask( callFfi: (port_) => _platform.inner.wire_lnurl_withdraw(port_, arg0, arg1, arg2), - parseSuccessData: _wire2api_ln_url_callback_status, + parseSuccessData: _wire2api_ln_url_withdraw_result, constMeta: kLnurlWithdrawConstMeta, argValues: [reqData, amountSats, description], hint: hint, @@ -2253,6 +2275,10 @@ class BreezSdkCoreImpl implements BreezSdkCore { return _wire2api_ln_url_withdraw_request_data(raw); } + LnUrlWithdrawSuccessData _wire2api_box_autoadd_ln_url_withdraw_success_data(dynamic raw) { + return _wire2api_ln_url_withdraw_success_data(raw); + } + LspInformation _wire2api_box_autoadd_lsp_information(dynamic raw) { return _wire2api_lsp_information(raw); } @@ -2546,7 +2572,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { LnPaymentDetails _wire2api_ln_payment_details(dynamic raw) { final arr = raw as List; - if (arr.length != 9) throw Exception('unexpected arr length: expect 9 but see ${arr.length}'); + if (arr.length != 10) throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); return LnPaymentDetails( paymentHash: _wire2api_String(arr[0]), label: _wire2api_String(arr[1]), @@ -2557,6 +2583,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { lnurlSuccessAction: _wire2api_opt_box_autoadd_success_action_processed(arr[6]), lnAddress: _wire2api_opt_String(arr[7]), lnurlMetadata: _wire2api_opt_String(arr[8]), + lnurlWithdrawEndpoint: _wire2api_opt_String(arr[9]), ); } @@ -2633,6 +2660,29 @@ class BreezSdkCoreImpl implements BreezSdkCore { ); } + LnUrlWithdrawResult _wire2api_ln_url_withdraw_result(dynamic raw) { + switch (raw[0]) { + case 0: + return LnUrlWithdrawResult_Ok( + data: _wire2api_box_autoadd_ln_url_withdraw_success_data(raw[1]), + ); + case 1: + return LnUrlWithdrawResult_ErrorStatus( + data: _wire2api_box_autoadd_ln_url_error_data(raw[1]), + ); + default: + throw Exception("unreachable"); + } + } + + LnUrlWithdrawSuccessData _wire2api_ln_url_withdraw_success_data(dynamic raw) { + final arr = raw as List; + if (arr.length != 1) throw Exception('unexpected arr length: expect 1 but see ${arr.length}'); + return LnUrlWithdrawSuccessData( + invoice: _wire2api_ln_invoice(arr[0]), + ); + } + LocaleOverrides _wire2api_locale_overrides(dynamic raw) { final arr = raw as List; if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); diff --git a/libs/sdk-flutter/lib/bridge_generated.freezed.dart b/libs/sdk-flutter/lib/bridge_generated.freezed.dart index 0d79fe4c8..659acf80a 100644 --- a/libs/sdk-flutter/lib/bridge_generated.freezed.dart +++ b/libs/sdk-flutter/lib/bridge_generated.freezed.dart @@ -3431,6 +3431,336 @@ abstract class LnUrlPayResult_EndpointError implements LnUrlPayResult { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$LnUrlWithdrawResult { + Object get data => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(LnUrlWithdrawSuccessData data) ok, + required TResult Function(LnUrlErrorData data) errorStatus, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(LnUrlWithdrawSuccessData data)? ok, + TResult? Function(LnUrlErrorData data)? errorStatus, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(LnUrlWithdrawSuccessData data)? ok, + TResult Function(LnUrlErrorData data)? errorStatus, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(LnUrlWithdrawResult_Ok value) ok, + required TResult Function(LnUrlWithdrawResult_ErrorStatus value) errorStatus, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LnUrlWithdrawResult_Ok value)? ok, + TResult? Function(LnUrlWithdrawResult_ErrorStatus value)? errorStatus, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LnUrlWithdrawResult_Ok value)? ok, + TResult Function(LnUrlWithdrawResult_ErrorStatus value)? errorStatus, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LnUrlWithdrawResultCopyWith<$Res> { + factory $LnUrlWithdrawResultCopyWith(LnUrlWithdrawResult value, $Res Function(LnUrlWithdrawResult) then) = + _$LnUrlWithdrawResultCopyWithImpl<$Res, LnUrlWithdrawResult>; +} + +/// @nodoc +class _$LnUrlWithdrawResultCopyWithImpl<$Res, $Val extends LnUrlWithdrawResult> + implements $LnUrlWithdrawResultCopyWith<$Res> { + _$LnUrlWithdrawResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$LnUrlWithdrawResult_OkCopyWith<$Res> { + factory _$$LnUrlWithdrawResult_OkCopyWith( + _$LnUrlWithdrawResult_Ok value, $Res Function(_$LnUrlWithdrawResult_Ok) then) = + __$$LnUrlWithdrawResult_OkCopyWithImpl<$Res>; + @useResult + $Res call({LnUrlWithdrawSuccessData data}); +} + +/// @nodoc +class __$$LnUrlWithdrawResult_OkCopyWithImpl<$Res> + extends _$LnUrlWithdrawResultCopyWithImpl<$Res, _$LnUrlWithdrawResult_Ok> + implements _$$LnUrlWithdrawResult_OkCopyWith<$Res> { + __$$LnUrlWithdrawResult_OkCopyWithImpl( + _$LnUrlWithdrawResult_Ok _value, $Res Function(_$LnUrlWithdrawResult_Ok) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? data = null, + }) { + return _then(_$LnUrlWithdrawResult_Ok( + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as LnUrlWithdrawSuccessData, + )); + } +} + +/// @nodoc + +class _$LnUrlWithdrawResult_Ok implements LnUrlWithdrawResult_Ok { + const _$LnUrlWithdrawResult_Ok({required this.data}); + + @override + final LnUrlWithdrawSuccessData data; + + @override + String toString() { + return 'LnUrlWithdrawResult.ok(data: $data)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LnUrlWithdrawResult_Ok && + (identical(other.data, data) || other.data == data)); + } + + @override + int get hashCode => Object.hash(runtimeType, data); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LnUrlWithdrawResult_OkCopyWith<_$LnUrlWithdrawResult_Ok> get copyWith => + __$$LnUrlWithdrawResult_OkCopyWithImpl<_$LnUrlWithdrawResult_Ok>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(LnUrlWithdrawSuccessData data) ok, + required TResult Function(LnUrlErrorData data) errorStatus, + }) { + return ok(data); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(LnUrlWithdrawSuccessData data)? ok, + TResult? Function(LnUrlErrorData data)? errorStatus, + }) { + return ok?.call(data); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(LnUrlWithdrawSuccessData data)? ok, + TResult Function(LnUrlErrorData data)? errorStatus, + required TResult orElse(), + }) { + if (ok != null) { + return ok(data); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LnUrlWithdrawResult_Ok value) ok, + required TResult Function(LnUrlWithdrawResult_ErrorStatus value) errorStatus, + }) { + return ok(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LnUrlWithdrawResult_Ok value)? ok, + TResult? Function(LnUrlWithdrawResult_ErrorStatus value)? errorStatus, + }) { + return ok?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LnUrlWithdrawResult_Ok value)? ok, + TResult Function(LnUrlWithdrawResult_ErrorStatus value)? errorStatus, + required TResult orElse(), + }) { + if (ok != null) { + return ok(this); + } + return orElse(); + } +} + +abstract class LnUrlWithdrawResult_Ok implements LnUrlWithdrawResult { + const factory LnUrlWithdrawResult_Ok({required final LnUrlWithdrawSuccessData data}) = + _$LnUrlWithdrawResult_Ok; + + @override + LnUrlWithdrawSuccessData get data; + @JsonKey(ignore: true) + _$$LnUrlWithdrawResult_OkCopyWith<_$LnUrlWithdrawResult_Ok> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LnUrlWithdrawResult_ErrorStatusCopyWith<$Res> { + factory _$$LnUrlWithdrawResult_ErrorStatusCopyWith( + _$LnUrlWithdrawResult_ErrorStatus value, $Res Function(_$LnUrlWithdrawResult_ErrorStatus) then) = + __$$LnUrlWithdrawResult_ErrorStatusCopyWithImpl<$Res>; + @useResult + $Res call({LnUrlErrorData data}); +} + +/// @nodoc +class __$$LnUrlWithdrawResult_ErrorStatusCopyWithImpl<$Res> + extends _$LnUrlWithdrawResultCopyWithImpl<$Res, _$LnUrlWithdrawResult_ErrorStatus> + implements _$$LnUrlWithdrawResult_ErrorStatusCopyWith<$Res> { + __$$LnUrlWithdrawResult_ErrorStatusCopyWithImpl( + _$LnUrlWithdrawResult_ErrorStatus _value, $Res Function(_$LnUrlWithdrawResult_ErrorStatus) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? data = null, + }) { + return _then(_$LnUrlWithdrawResult_ErrorStatus( + data: null == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as LnUrlErrorData, + )); + } +} + +/// @nodoc + +class _$LnUrlWithdrawResult_ErrorStatus implements LnUrlWithdrawResult_ErrorStatus { + const _$LnUrlWithdrawResult_ErrorStatus({required this.data}); + + @override + final LnUrlErrorData data; + + @override + String toString() { + return 'LnUrlWithdrawResult.errorStatus(data: $data)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LnUrlWithdrawResult_ErrorStatus && + (identical(other.data, data) || other.data == data)); + } + + @override + int get hashCode => Object.hash(runtimeType, data); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LnUrlWithdrawResult_ErrorStatusCopyWith<_$LnUrlWithdrawResult_ErrorStatus> get copyWith => + __$$LnUrlWithdrawResult_ErrorStatusCopyWithImpl<_$LnUrlWithdrawResult_ErrorStatus>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(LnUrlWithdrawSuccessData data) ok, + required TResult Function(LnUrlErrorData data) errorStatus, + }) { + return errorStatus(data); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(LnUrlWithdrawSuccessData data)? ok, + TResult? Function(LnUrlErrorData data)? errorStatus, + }) { + return errorStatus?.call(data); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(LnUrlWithdrawSuccessData data)? ok, + TResult Function(LnUrlErrorData data)? errorStatus, + required TResult orElse(), + }) { + if (errorStatus != null) { + return errorStatus(data); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LnUrlWithdrawResult_Ok value) ok, + required TResult Function(LnUrlWithdrawResult_ErrorStatus value) errorStatus, + }) { + return errorStatus(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LnUrlWithdrawResult_Ok value)? ok, + TResult? Function(LnUrlWithdrawResult_ErrorStatus value)? errorStatus, + }) { + return errorStatus?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LnUrlWithdrawResult_Ok value)? ok, + TResult Function(LnUrlWithdrawResult_ErrorStatus value)? errorStatus, + required TResult orElse(), + }) { + if (errorStatus != null) { + return errorStatus(this); + } + return orElse(); + } +} + +abstract class LnUrlWithdrawResult_ErrorStatus implements LnUrlWithdrawResult { + const factory LnUrlWithdrawResult_ErrorStatus({required final LnUrlErrorData data}) = + _$LnUrlWithdrawResult_ErrorStatus; + + @override + LnUrlErrorData get data; + @JsonKey(ignore: true) + _$$LnUrlWithdrawResult_ErrorStatusCopyWith<_$LnUrlWithdrawResult_ErrorStatus> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$NodeConfig { GreenlightNodeConfig get config => throw _privateConstructorUsedError; diff --git a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt index 1d593187d..4b2dc9005 100644 --- a/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt +++ b/libs/sdk-react-native/android/src/main/java/com/breezsdk/BreezSDKMapper.kt @@ -773,6 +773,7 @@ fun asLnPaymentDetails(data: ReadableMap): LnPaymentDetails? { } val lnurlMetadata = if (hasNonNullKey(data, "lnurlMetadata")) data.getString("lnurlMetadata") else null val lnAddress = if (hasNonNullKey(data, "lnAddress")) data.getString("lnAddress") else null + val lnurlWithdrawEndpoint = if (hasNonNullKey(data, "lnurlWithdrawEndpoint")) data.getString("lnurlWithdrawEndpoint") else null return LnPaymentDetails( paymentHash, label, @@ -783,6 +784,7 @@ fun asLnPaymentDetails(data: ReadableMap): LnPaymentDetails? { lnurlSuccessAction, lnurlMetadata, lnAddress, + lnurlWithdrawEndpoint, ) } @@ -797,6 +799,7 @@ fun readableMapOf(lnPaymentDetails: LnPaymentDetails): ReadableMap { "lnurlSuccessAction" to lnPaymentDetails.lnurlSuccessAction?.let { readableMapOf(it) }, "lnurlMetadata" to lnPaymentDetails.lnurlMetadata, "lnAddress" to lnPaymentDetails.lnAddress, + "lnurlWithdrawEndpoint" to lnPaymentDetails.lnurlWithdrawEndpoint, ) } @@ -993,6 +996,39 @@ fun asLnUrlWithdrawRequestDataList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toArrayList()) { + when (value) { + is ReadableMap -> list.add(asLnUrlWithdrawSuccessData(value)!!) + else -> throw IllegalArgumentException("Unsupported type ${value::class.java.name}") + } + } + return list +} + fun asLocaleOverrides(data: ReadableMap): LocaleOverrides? { if (!validateMandatoryFields( data, @@ -2621,6 +2657,33 @@ fun readableMapOf(lnUrlPayResult: LnUrlPayResult): ReadableMap? { return map } +fun asLnUrlWithdrawResult(data: ReadableMap): LnUrlWithdrawResult? { + val type = data.getString("type") + + if (type == "ok") { + return LnUrlWithdrawResult.Ok(data.getMap("data")?.let { asLnUrlWithdrawSuccessData(it) }!!) + } + if (type == "errorStatus") { + return LnUrlWithdrawResult.ErrorStatus(data.getMap("data")?.let { asLnUrlErrorData(it) }!!) + } + return null +} + +fun readableMapOf(lnUrlWithdrawResult: LnUrlWithdrawResult): ReadableMap? { + val map = Arguments.createMap() + when (lnUrlWithdrawResult) { + is LnUrlWithdrawResult.Ok -> { + pushToMap(map, "type", "ok") + pushToMap(map, "data", readableMapOf(lnUrlWithdrawResult.data)) + } + is LnUrlWithdrawResult.ErrorStatus -> { + pushToMap(map, "type", "errorStatus") + pushToMap(map, "data", readableMapOf(lnUrlWithdrawResult.data)) + } + } + return map +} + fun asNetwork(type: String): Network { return Network.valueOf(type.uppercase()) } diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index e8eaee0c8..6738321ba 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -709,6 +709,7 @@ class BreezSDKMapper { let lnurlMetadata = data["lnurlMetadata"] as? String let lnAddress = data["lnAddress"] as? String + let lnurlWithdrawEndpoint = data["lnurlWithdrawEndpoint"] as? String return LnPaymentDetails( paymentHash: paymentHash, @@ -719,7 +720,8 @@ class BreezSDKMapper { bolt11: bolt11, lnurlSuccessAction: lnurlSuccessAction, lnurlMetadata: lnurlMetadata, - lnAddress: lnAddress + lnAddress: lnAddress, + lnurlWithdrawEndpoint: lnurlWithdrawEndpoint ) } @@ -734,6 +736,7 @@ class BreezSDKMapper { "lnurlSuccessAction": lnPaymentDetails.lnurlSuccessAction == nil ? nil : dictionaryOf(successActionProcessed: lnPaymentDetails.lnurlSuccessAction!), "lnurlMetadata": lnPaymentDetails.lnurlMetadata == nil ? nil : lnPaymentDetails.lnurlMetadata, "lnAddress": lnPaymentDetails.lnAddress == nil ? nil : lnPaymentDetails.lnAddress, + "lnurlWithdrawEndpoint": lnPaymentDetails.lnurlWithdrawEndpoint == nil ? nil : lnPaymentDetails.lnurlWithdrawEndpoint, ] } @@ -916,6 +919,37 @@ class BreezSDKMapper { return lnUrlWithdrawRequestDataList.map { v -> [String: Any?] in dictionaryOf(lnUrlWithdrawRequestData: v) } } + static func asLnUrlWithdrawSuccessData(data: [String: Any?]) throws -> LnUrlWithdrawSuccessData { + guard let invoiceTmp = data["invoice"] as? [String: Any?] else { throw SdkError.Generic(message: "Missing mandatory field invoice for type LnUrlWithdrawSuccessData") } + let invoice = try asLnInvoice(data: invoiceTmp) + + return LnUrlWithdrawSuccessData( + invoice: invoice) + } + + static func dictionaryOf(lnUrlWithdrawSuccessData: LnUrlWithdrawSuccessData) -> [String: Any?] { + return [ + "invoice": dictionaryOf(lnInvoice: lnUrlWithdrawSuccessData.invoice), + ] + } + + static func asLnUrlWithdrawSuccessDataList(arr: [Any]) throws -> [LnUrlWithdrawSuccessData] { + var list = [LnUrlWithdrawSuccessData]() + for value in arr { + if let val = value as? [String: Any?] { + var lnUrlWithdrawSuccessData = try asLnUrlWithdrawSuccessData(data: val) + list.append(lnUrlWithdrawSuccessData) + } else { + throw SdkError.Generic(message: "Invalid element type LnUrlWithdrawSuccessData") + } + } + return list + } + + static func arrayOf(lnUrlWithdrawSuccessDataList: [LnUrlWithdrawSuccessData]) -> [Any] { + return lnUrlWithdrawSuccessDataList.map { v -> [String: Any?] in dictionaryOf(lnUrlWithdrawSuccessData: v) } + } + static func asLocaleOverrides(data: [String: Any?]) throws -> LocaleOverrides { guard let locale = data["locale"] as? String else { throw SdkError.Generic(message: "Missing mandatory field locale for type LocaleOverrides") } let spacing = data["spacing"] as? UInt32 @@ -2596,6 +2630,44 @@ class BreezSDKMapper { } } + static func asLnUrlWithdrawResult(data: [String: Any?]) throws -> LnUrlWithdrawResult { + let type = data["type"] as! String + if type == "ok" { + guard let dataTmp = data["data"] as? [String: Any?] else { throw SdkError.Generic(message: "Missing mandatory field data for type LnUrlWithdrawResult") } + let _data = try asLnUrlWithdrawSuccessData(data: dataTmp) + + return LnUrlWithdrawResult.ok(data: _data) + } + if type == "errorStatus" { + guard let dataTmp = data["data"] as? [String: Any?] else { throw SdkError.Generic(message: "Missing mandatory field data for type LnUrlWithdrawResult") } + let _data = try asLnUrlErrorData(data: dataTmp) + + return LnUrlWithdrawResult.errorStatus(data: _data) + } + + throw SdkError.Generic(message: "Invalid enum variant \(type) for enum LnUrlWithdrawResult") + } + + static func dictionaryOf(lnUrlWithdrawResult: LnUrlWithdrawResult) -> [String: Any?] { + switch lnUrlWithdrawResult { + case let .ok( + data + ): + return [ + "type": "ok", + "data": dictionaryOf(lnUrlWithdrawSuccessData: data), + ] + + case let .errorStatus( + data + ): + return [ + "type": "errorStatus", + "data": dictionaryOf(lnUrlErrorData: data), + ] + } + } + static func asNetwork(type: String) throws -> Network { switch type { case "bitcoin": diff --git a/libs/sdk-react-native/ios/RNBreezSDK.swift b/libs/sdk-react-native/ios/RNBreezSDK.swift index 08722b496..33fb45ed3 100644 --- a/libs/sdk-react-native/ios/RNBreezSDK.swift +++ b/libs/sdk-react-native/ios/RNBreezSDK.swift @@ -180,7 +180,7 @@ class RNBreezSDK: RCTEventEmitter { let lnUrlWithdrawRequestData = try BreezSDKMapper.asLnUrlWithdrawRequestData(data: reqData) let descriptionTmp = description.isEmpty ? nil : description var res = try getBreezServices().withdrawLnurl(reqData: lnUrlWithdrawRequestData, amountSats: amountSats, description: descriptionTmp) - resolve(BreezSDKMapper.dictionaryOf(lnUrlCallbackStatus: res)) + resolve(BreezSDKMapper.dictionaryOf(lnUrlWithdrawResult: res)) } catch let err { rejectErr(err: err, reject: reject) } diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index 105730e60..a67eb9d7b 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -140,6 +140,7 @@ export type LnPaymentDetails = { lnurlSuccessAction?: SuccessActionProcessed lnurlMetadata?: string lnAddress?: string + lnurlWithdrawEndpoint?: string } export type LnUrlAuthRequestData = { @@ -171,6 +172,10 @@ export type LnUrlWithdrawRequestData = { maxWithdrawable: number } +export type LnUrlWithdrawSuccessData = { + invoice: LnInvoice +} + export type LocaleOverrides = { locale: string spacing?: number @@ -513,6 +518,19 @@ export type LnUrlPayResult = { data: LnUrlErrorData } +export enum LnUrlWithdrawResultVariant { + OK = "ok", + ERROR_STATUS = "errorStatus" +} + +export type LnUrlWithdrawResult = { + type: LnUrlWithdrawResultVariant.OK, + data: LnUrlWithdrawSuccessData +} | { + type: LnUrlWithdrawResultVariant.ERROR_STATUS, + data: LnUrlErrorData +} + export enum Network { BITCOIN = "bitcoin", TESTNET = "testnet", @@ -659,7 +677,7 @@ export const payLnurl = async (reqData: LnUrlPayRequestData, amountSats: number, return response } -export const withdrawLnurl = async (reqData: LnUrlWithdrawRequestData, amountSats: number, description: string = ""): Promise => { +export const withdrawLnurl = async (reqData: LnUrlWithdrawRequestData, amountSats: number, description: string = ""): Promise => { const response = await BreezSDK.withdrawLnurl(reqData, amountSats, description) return response }