diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl index c3cf8bf7a..6855238c7 100644 --- a/libs/sdk-bindings/src/breez_sdk.udl +++ b/libs/sdk-bindings/src/breez_sdk.udl @@ -292,6 +292,7 @@ dictionary LnPaymentDetails { string payment_preimage; boolean keysend; string bolt11; + string? open_channel_bolt11; SuccessActionProcessed? lnurl_success_action; string? lnurl_pay_domain; string? lnurl_metadata; diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs index 2b01285e6..5e0979c58 100644 --- a/libs/sdk-core/src/breez_services.rs +++ b/libs/sdk-core/src/breez_services.rs @@ -1020,6 +1020,7 @@ impl BreezServices { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -2065,7 +2066,7 @@ impl Receiver for PaymentReceiver { } info!("Creating invoice on NodeAPI"); - let invoice = &self + let mut invoice = self .node_api .create_invoice( destination_invoice_amount_msat, @@ -2078,7 +2079,7 @@ impl Receiver for PaymentReceiver { .await?; info!("Invoice created {}", invoice); - let mut parsed_invoice = parse_invoice(invoice)?; + let mut parsed_invoice = parse_invoice(&invoice)?; // check if the lsp hint already exists info!("Existing routing hints {:?}", parsed_invoice.routing_hints); @@ -2143,9 +2144,9 @@ impl Receiver for PaymentReceiver { }; if let Some(raw_invoice) = optional_modified_invoice { - let signed_invoice = self.node_api.sign_invoice(raw_invoice)?; - info!("Signed invoice with hint = {}", signed_invoice); - parsed_invoice = parse_invoice(&signed_invoice)?; + invoice = self.node_api.sign_invoice(raw_invoice)?; + info!("Signed invoice with hint = {}", invoice); + parsed_invoice = parse_invoice(&invoice)?; } // register the payment at the lsp if needed @@ -2181,8 +2182,11 @@ impl Receiver for PaymentReceiver { .await?; info!("Payment registered"); // Make sure we save the large amount so we can deduce the fees later. - self.persister - .insert_open_channel_payment_info(&parsed_invoice.payment_hash, req.amount_msat)?; + self.persister.insert_open_channel_payment_info( + &parsed_invoice.payment_hash, + req.amount_msat, + &invoice, + )?; } // return the signed, converted invoice with hints @@ -2371,6 +2375,7 @@ pub(crate) mod tests { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -2400,6 +2405,7 @@ pub(crate) mod tests { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -2429,6 +2435,7 @@ pub(crate) mod tests { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -2458,6 +2465,7 @@ pub(crate) mod tests { swap_info: Some(swap_info.clone()), reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -2487,6 +2495,7 @@ pub(crate) mod tests { swap_info: None, reverse_swap_info: Some(rev_swap_info.clone()), pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs index 98291c114..51c0ba649 100644 --- a/libs/sdk-core/src/bridge_generated.rs +++ b/libs/sdk-core/src/bridge_generated.rs @@ -1293,6 +1293,7 @@ impl support::IntoDart for LnPaymentDetails { self.payment_preimage.into_into_dart().into_dart(), 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_pay_domain.into_dart(), self.ln_address.into_dart(), diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs index d64e823bd..1ad46a5bc 100644 --- a/libs/sdk-core/src/greenlight/node_api.rs +++ b/libs/sdk-core/src/greenlight/node_api.rs @@ -1662,6 +1662,7 @@ impl TryFrom for Payment { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -1705,6 +1706,7 @@ impl TryFrom for Payment { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -1763,6 +1765,7 @@ impl TryFrom for Payment { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -1808,6 +1811,7 @@ impl TryFrom for Payment { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -1876,6 +1880,7 @@ impl TryFrom for Payment { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs index d6b361a2b..146305a9b 100644 --- a/libs/sdk-core/src/models.rs +++ b/libs/sdk-core/src/models.rs @@ -732,6 +732,10 @@ pub struct LnPaymentDetails { pub keysend: bool, pub bolt11: String, + /// Only set for [PaymentType::Received], payments which require to open a channel. + /// Represents the actual invoice paid by the sender + pub open_channel_bolt11: Option, + /// Only set for [PaymentType::Sent] payments that are part of a LNURL-pay workflow where /// the endpoint returns a success action pub lnurl_success_action: Option, diff --git a/libs/sdk-core/src/persist/migrations.rs b/libs/sdk-core/src/persist/migrations.rs index 0104645ba..a9d8bab42 100644 --- a/libs/sdk-core/src/persist/migrations.rs +++ b/libs/sdk-core/src/persist/migrations.rs @@ -555,5 +555,6 @@ pub(crate) fn current_sync_migrations() -> Vec<&'static str> { ) STRICT; ", "ALTER TABLE payments_external_info ADD COLUMN lnurl_pay_domain TEXT;", + "ALTER TABLE open_channel_payment_info ADD COLUMN open_channel_bolt11 TEXT;", ] } diff --git a/libs/sdk-core/src/persist/sync.rs b/libs/sdk-core/src/persist/sync.rs index 936cb795c..e78058855 100644 --- a/libs/sdk-core/src/persist/sync.rs +++ b/libs/sdk-core/src/persist/sync.rs @@ -219,7 +219,8 @@ impl SqliteStorage { INSERT INTO sync.open_channel_payment_info SELECT payment_hash, - payer_amount_msat + payer_amount_msat, + open_channel_bolt11 FROM remote_sync.open_channel_payment_info WHERE payment_hash NOT IN (SELECT payment_hash FROM sync.open_channel_payment_info);", [], @@ -294,7 +295,7 @@ mod tests { remote_storage.init()?; remote_storage.insert_swap(remote_swap_info)?; - remote_storage.insert_open_channel_payment_info("123", 100000)?; + remote_storage.insert_open_channel_payment_info("123", 100000, "")?; remote_storage.import_remote_changes(&local_storage, false)?; local_storage.import_remote_changes(&remote_storage, true)?; diff --git a/libs/sdk-core/src/persist/transactions.rs b/libs/sdk-core/src/persist/transactions.rs index 976319ada..9282268ce 100644 --- a/libs/sdk-core/src/persist/transactions.rs +++ b/libs/sdk-core/src/persist/transactions.rs @@ -173,19 +173,21 @@ impl SqliteStorage { &self, payment_hash: &str, payer_amount_msat: u64, + open_channel_bolt11: &str, ) -> PersistResult<()> { let con = self.get_connection()?; let mut prep_statement = con.prepare( " INSERT INTO sync.open_channel_payment_info ( payment_hash, - payer_amount_msat + payer_amount_msat, + open_channel_bolt11 ) - VALUES (?1,?2) + VALUES (?1,?2,?3) ", )?; - _ = prep_statement.execute((payment_hash, payer_amount_msat))?; + _ = prep_statement.execute((payment_hash, payer_amount_msat, open_channel_bolt11))?; Ok(()) } @@ -232,6 +234,7 @@ impl SqliteStorage { e.attempted_amount_msat, e.attempted_error, o.payer_amount_msat, + o.open_channel_bolt11, m.metadata, e.lnurl_pay_domain FROM payments p @@ -310,6 +313,7 @@ impl SqliteStorage { e.attempted_amount_msat, e.attempted_error, o.payer_amount_msat, + o.open_channel_bolt11, m.metadata, e.lnurl_pay_domain FROM payments p @@ -361,12 +365,12 @@ impl SqliteStorage { description: row.get(6)?, details: row.get(7)?, error: row.get(13)?, - metadata: row.get(15)?, + metadata: row.get(16)?, }; if let PaymentDetails::Ln { ref mut data } = payment.details { data.lnurl_success_action = row.get(8)?; - data.lnurl_pay_domain = row.get(16)?; + data.lnurl_pay_domain = row.get(17)?; data.lnurl_metadata = row.get(9)?; data.ln_address = row.get(10)?; data.lnurl_withdraw_endpoint = row.get(11)?; @@ -387,6 +391,13 @@ impl SqliteStorage { payment.fee_msat = payer_amount - amount_msat; } + // Add the payer invoice if it exists, in case of a received payment + if let Some(open_channel_bolt11) = row.get(15)? { + if let PaymentDetails::Ln { data } = &mut payment.details { + data.open_channel_bolt11 = Some(open_channel_bolt11); + } + } + Ok(payment) } } @@ -605,6 +616,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -634,6 +646,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -663,6 +676,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { swap_info: Some(swap_info.clone()), reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -692,6 +706,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { swap_info: None, reverse_swap_info: Some(rev_swap_info.clone()), pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -721,6 +736,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -751,6 +767,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, @@ -854,7 +871,7 @@ fn test_ln_transactions() -> PersistResult<(), Box> { assert_eq!(retrieve_txs.len(), 5); assert_eq!(retrieve_txs, txs); - storage.insert_open_channel_payment_info("123", 150)?; + storage.insert_open_channel_payment_info("123", 150, "")?; let retrieve_txs = storage.list_payments(ListPaymentsRequest::default())?; assert_eq!(retrieve_txs[0].fee_msat, 50); @@ -936,5 +953,36 @@ fn test_ln_transactions() -> PersistResult<(), Box> { assert_eq!(retrieve_txs[0].id, payment_hash_with_lnurl_withdraw); assert_eq!(retrieve_txs[0].metadata, Some(test_json.to_string()),); + // test open_channel_bolt11 + storage.insert_open_channel_payment_info( + payment_hash_with_lnurl_withdraw, + 150, + "original_invoice", + )?; + let retrieve_txs = storage.list_payments(ListPaymentsRequest { + filters: Some(vec![PaymentTypeFilter::Received]), + ..Default::default() + })?; + + let filtered_txs: Vec<&Payment> = retrieve_txs + .iter() + .filter(|p| { + if let PaymentDetails::Ln { data } = &p.details { + return data.open_channel_bolt11 == Some("original_invoice".to_string()); + } + false + }) + .collect(); + + assert_eq!(filtered_txs.len(), 1); + assert_eq!(filtered_txs[0].id, payment_hash_with_lnurl_withdraw); + assert!(matches!(filtered_txs[0].details, PaymentDetails::Ln { .. })); + if let PaymentDetails::Ln { data } = &filtered_txs[0].details { + assert_eq!( + data.open_channel_bolt11, + Some("original_invoice".to_string()) + ); + } + Ok(()) } diff --git a/libs/sdk-core/src/swap_in/swap.rs b/libs/sdk-core/src/swap_in/swap.rs index c945bc556..1f84a04b3 100644 --- a/libs/sdk-core/src/swap_in/swap.rs +++ b/libs/sdk-core/src/swap_in/swap.rs @@ -880,6 +880,7 @@ mod tests { swap_info: None, reverse_swap_info: None, pending_expiration_block: None, + open_channel_bolt11: None, }, }, metadata: None, diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart index eb8a083db..072ce84f4 100644 --- a/libs/sdk-flutter/lib/bridge_generated.dart +++ b/libs/sdk-flutter/lib/bridge_generated.dart @@ -717,6 +717,10 @@ class LnPaymentDetails { final bool keysend; final String bolt11; + /// Only set for [PaymentType::Received], payments which require to open a channel. + /// Represents the actual invoice paid by the sender + final String? openChannelBolt11; + /// Only set for [PaymentType::Sent] payments that are part of a LNURL-pay workflow where /// the endpoint returns a success action final SuccessActionProcessed? lnurlSuccessAction; @@ -749,6 +753,7 @@ class LnPaymentDetails { required this.paymentPreimage, required this.keysend, required this.bolt11, + this.openChannelBolt11, this.lnurlSuccessAction, this.lnurlPayDomain, this.lnAddress, @@ -3266,7 +3271,7 @@ class BreezSdkCoreImpl implements BreezSdkCore { LnPaymentDetails _wire2api_ln_payment_details(dynamic raw) { final arr = raw as List; - if (arr.length != 14) throw Exception('unexpected arr length: expect 14 but see ${arr.length}'); + if (arr.length != 15) throw Exception('unexpected arr length: expect 15 but see ${arr.length}'); return LnPaymentDetails( paymentHash: _wire2api_String(arr[0]), label: _wire2api_String(arr[1]), @@ -3274,14 +3279,15 @@ class BreezSdkCoreImpl implements BreezSdkCore { paymentPreimage: _wire2api_String(arr[3]), keysend: _wire2api_bool(arr[4]), bolt11: _wire2api_String(arr[5]), - lnurlSuccessAction: _wire2api_opt_box_autoadd_success_action_processed(arr[6]), - lnurlPayDomain: _wire2api_opt_String(arr[7]), - lnAddress: _wire2api_opt_String(arr[8]), - lnurlMetadata: _wire2api_opt_String(arr[9]), - lnurlWithdrawEndpoint: _wire2api_opt_String(arr[10]), - swapInfo: _wire2api_opt_box_autoadd_swap_info(arr[11]), - reverseSwapInfo: _wire2api_opt_box_autoadd_reverse_swap_info(arr[12]), - pendingExpirationBlock: _wire2api_opt_box_autoadd_u32(arr[13]), + openChannelBolt11: _wire2api_opt_String(arr[6]), + lnurlSuccessAction: _wire2api_opt_box_autoadd_success_action_processed(arr[7]), + lnurlPayDomain: _wire2api_opt_String(arr[8]), + lnAddress: _wire2api_opt_String(arr[9]), + lnurlMetadata: _wire2api_opt_String(arr[10]), + lnurlWithdrawEndpoint: _wire2api_opt_String(arr[11]), + swapInfo: _wire2api_opt_box_autoadd_swap_info(arr[12]), + reverseSwapInfo: _wire2api_opt_box_autoadd_reverse_swap_info(arr[13]), + pendingExpirationBlock: _wire2api_opt_box_autoadd_u32(arr[14]), ); } 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 53d5da3e5..26ea0142f 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 @@ -888,6 +888,16 @@ fun asLnPaymentDetails(lnPaymentDetails: ReadableMap): LnPaymentDetails? { val paymentPreimage = lnPaymentDetails.getString("paymentPreimage")!! val keysend = lnPaymentDetails.getBoolean("keysend") val bolt11 = lnPaymentDetails.getString("bolt11")!! + val openChannelBolt11 = + if (hasNonNullKey( + lnPaymentDetails, + "openChannelBolt11", + ) + ) { + lnPaymentDetails.getString("openChannelBolt11") + } else { + null + } val lnurlSuccessAction = if (hasNonNullKey(lnPaymentDetails, "lnurlSuccessAction")) { lnPaymentDetails.getMap("lnurlSuccessAction")?.let { @@ -935,6 +945,7 @@ fun asLnPaymentDetails(lnPaymentDetails: ReadableMap): LnPaymentDetails? { paymentPreimage, keysend, bolt11, + openChannelBolt11, lnurlSuccessAction, lnurlPayDomain, lnurlMetadata, @@ -954,6 +965,7 @@ fun readableMapOf(lnPaymentDetails: LnPaymentDetails): ReadableMap { "paymentPreimage" to lnPaymentDetails.paymentPreimage, "keysend" to lnPaymentDetails.keysend, "bolt11" to lnPaymentDetails.bolt11, + "openChannelBolt11" to lnPaymentDetails.openChannelBolt11, "lnurlSuccessAction" to lnPaymentDetails.lnurlSuccessAction?.let { readableMapOf(it) }, "lnurlPayDomain" to lnPaymentDetails.lnurlPayDomain, "lnurlMetadata" to lnPaymentDetails.lnurlMetadata, diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift index a5c7fdf43..08f5e9769 100644 --- a/libs/sdk-react-native/ios/BreezSDKMapper.swift +++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift @@ -973,6 +973,13 @@ enum BreezSDKMapper { guard let bolt11 = lnPaymentDetails["bolt11"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "bolt11", typeName: "LnPaymentDetails")) } + var openChannelBolt11: String? + if hasNonNilKey(data: lnPaymentDetails, key: "openChannelBolt11") { + guard let openChannelBolt11Tmp = lnPaymentDetails["openChannelBolt11"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "openChannelBolt11")) + } + openChannelBolt11 = openChannelBolt11Tmp + } var lnurlSuccessAction: SuccessActionProcessed? if let lnurlSuccessActionTmp = lnPaymentDetails["lnurlSuccessAction"] as? [String: Any?] { lnurlSuccessAction = try asSuccessActionProcessed(successActionProcessed: lnurlSuccessActionTmp) @@ -1031,6 +1038,7 @@ enum BreezSDKMapper { paymentPreimage: paymentPreimage, keysend: keysend, bolt11: bolt11, + openChannelBolt11: openChannelBolt11, lnurlSuccessAction: lnurlSuccessAction, lnurlPayDomain: lnurlPayDomain, lnurlMetadata: lnurlMetadata, @@ -1050,6 +1058,7 @@ enum BreezSDKMapper { "paymentPreimage": lnPaymentDetails.paymentPreimage, "keysend": lnPaymentDetails.keysend, "bolt11": lnPaymentDetails.bolt11, + "openChannelBolt11": lnPaymentDetails.openChannelBolt11 == nil ? nil : lnPaymentDetails.openChannelBolt11, "lnurlSuccessAction": lnPaymentDetails.lnurlSuccessAction == nil ? nil : dictionaryOf(successActionProcessed: lnPaymentDetails.lnurlSuccessAction!), "lnurlPayDomain": lnPaymentDetails.lnurlPayDomain == nil ? nil : lnPaymentDetails.lnurlPayDomain, "lnurlMetadata": lnPaymentDetails.lnurlMetadata == nil ? nil : lnPaymentDetails.lnurlMetadata, diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts index d0bbf0436..bc0d3e51a 100644 --- a/libs/sdk-react-native/src/index.ts +++ b/libs/sdk-react-native/src/index.ts @@ -149,6 +149,7 @@ export type LnPaymentDetails = { paymentPreimage: string keysend: boolean bolt11: string + openChannelBolt11?: string lnurlSuccessAction?: SuccessActionProcessed lnurlPayDomain?: string lnurlMetadata?: string