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

Issue:
Payment asynchronicity
Hi all,

i've been using the Breez SDK to implement an app that segregates the underlying node into accounts. Similar to LNBits, it is possible to create multiple accounts that share the same underlying node, each with its own balance and permissions.

When implementing the "outgoing payment" functionality, I noticed that I can run into concurrency issues between the moment the payment is sent and the moment the event is received because the sending function returns the payment_hash only after the payment has been processed.

In order to implement the account functionality, I need to correlate the payment with a user account, so I need to save the payment in DB and then be able to find it back once it is received in an event. The reason is that if a payment is sent and then we crash, once the event is read upon restart, we won't be able to correlate it with a user. 

What I would like:
- A way to add labels to a payment so I can add a unique payment identifier that is different than the `payment_hash`, and retrieve it in the event that would have these labels `attached`.
- An asynchronous function that sends the payment but does not wait for its success and returns the `payment_hash`.
- any a similar solution enabling payment asynchronicity.

I noticed that the `Payment` returned when the function is successful in `LnUrlPayResult` does not contain all the `Payment` details in a similar way than the event `PaymentSucceed`, so I need the event even in the event where we implement it synchronously. 


Discussion / hints:
Hi @darioAnongba you can save the payment hash before invoking send by parsing the paid bolt11.
Will that work?
Hi,

The `LnUrlPayRequestData` does not contain a `payment_hash` field, nor does the `InputType::NodeId`. It seems this will only work for InputType::Bolt11. I don't have a unique identifier after using the `parse` function for each type of input. Adding personal labels would be good.
@roeierez would it help in this case to allow adding optional metadata to the `LnUrlPayRequest` which can then be added to the payment internally once the payment is complete (and before the event is triggered)?
> @roeierez would it help in this case to allow adding optional metadata to the `LnUrlPayRequest` which can then be added to the payment internally once the payment is complete (and before the event is triggered)?

I think yes but for consistency we should add this optional field also to the other payment methods (bolt11, keyse d)

Fix patch:
diff --git a/libs/sdk-bindings/src/breez_sdk.udl b/libs/sdk-bindings/src/breez_sdk.udl
index 965b958de..6e6b793b1 100644
--- a/libs/sdk-bindings/src/breez_sdk.udl
+++ b/libs/sdk-bindings/src/breez_sdk.udl
@@ -600,7 +600,7 @@ dictionary BitcoinAddressData {
 
 dictionary LnUrlPaySuccessData {
     SuccessActionProcessed? success_action;
-    string payment_hash;
+    Payment payment;
 };
 
 dictionary LnUrlErrorData {
@@ -616,6 +616,7 @@ dictionary LnUrlPayRequest {
     LnUrlPayRequestData data;
     u64 amount_msat;
     string? comment = null;
+    string? payment_label = null;
 };
 
 dictionary LnUrlPayRequestData {
@@ -743,12 +744,14 @@ dictionary RedeemOnchainFundsResponse {
 dictionary SendPaymentRequest {
     string bolt11;
     u64? amount_msat = null;
+    string? label = null;
 };
 
 dictionary SendSpontaneousPaymentRequest {
     string node_id;
     u64 amount_msat;
     sequence<TlvEntry>? extra_tlvs = null;
+    string? label = null;
 };
 
 dictionary SendPaymentResponse {
diff --git a/libs/sdk-core/src/breez_services.rs b/libs/sdk-core/src/breez_services.rs
index ad80ca8c1..f9bb122ea 100644
--- a/libs/sdk-core/src/breez_services.rs
+++ b/libs/sdk-core/src/breez_services.rs
@@ -313,10 +313,10 @@ impl BreezServices {
         {
             Some(_) => Err(SendPaymentError::AlreadyPaid),
             None => {
-                self.persist_pending_payment(&parsed_invoice, amount_msat)?;
+                self.persist_pending_payment(&parsed_invoice, amount_msat, req.label.clone())?;
                 let payment_res = self
                     .node_api
-                    .send_payment(parsed_invoice.bolt11.clone(), req.amount_msat)
+                    .send_payment(parsed_invoice.bolt11.clone(), req.amount_msat, req.label)
                     .map_err(Into::into)
                     .await;
                 let payment = self
@@ -339,7 +339,12 @@ impl BreezServices {
         self.start_node().await?;
         let payment_res = self
             .node_api
-            .send_spontaneous_payment(req.node_id.clone(), req.amount_msat, req.extra_tlvs)
+            .send_spontaneous_payment(
+                req.node_id.clone(),
+                req.amount_msat,
+                req.extra_tlvs,
+                req.label,
+            )
             .map_err(Into::into)
             .await;
         let payment = self
@@ -372,6 +377,7 @@ impl BreezServices {
                 let pay_req = SendPaymentRequest {
                     bolt11: cb.pr.clone(),
                     amount_msat: None,
+                    label: req.payment_label,
                 };
                 let invoice = parse_invoice(cb.pr.as_str())?;
 
@@ -445,7 +451,7 @@ impl BreezServices {
 
                 Ok(LnUrlPayResult::EndpointSuccess {
                     data: LnUrlPaySuccessData {
-                        payment_hash: details.payment_hash.clone(),
+                        payment,
                         success_action: maybe_sa_processed,
                     },
                 })
@@ -1158,6 +1164,7 @@ impl BreezServices {
         &self,
         invoice: &LNInvoice,
         amount_msat: u64,
+        label: Option<String>,
     ) -> Result<(), SendPaymentError> {
         self.persister.insert_or_update_payments(
             &[Payment {
@@ -1172,7 +1179,7 @@ impl BreezServices {
                 details: PaymentDetails::Ln {
                     data: LnPaymentDetails {
                         payment_hash: invoice.payment_hash.clone(),
-                        label: String::new(),
+                        label: label.unwrap_or_default(),
                         destination_pubkey: invoice.payee_pubkey.clone(),
                         payment_preimage: String::new(),
                         keysend: false,
diff --git a/libs/sdk-core/src/bridge_generated.io.rs b/libs/sdk-core/src/bridge_generated.io.rs
index 392691838..ee3b4cca3 100644
--- a/libs/sdk-core/src/bridge_generated.io.rs
+++ b/libs/sdk-core/src/bridge_generated.io.rs
@@ -858,6 +858,7 @@ impl Wire2Api<LnUrlPayRequest> for wire_LnUrlPayRequest {
             data: self.data.wire2api(),
             amount_msat: self.amount_msat.wire2api(),
             comment: self.comment.wire2api(),
+            payment_label: self.payment_label.wire2api(),
         }
     }
 }
@@ -1068,6 +1069,7 @@ impl Wire2Api<SendPaymentRequest> for wire_SendPaymentRequest {
         SendPaymentRequest {
             bolt11: self.bolt11.wire2api(),
             amount_msat: self.amount_msat.wire2api(),
+            label: self.label.wire2api(),
         }
     }
 }
@@ -1077,6 +1079,7 @@ impl Wire2Api<SendSpontaneousPaymentRequest> for wire_SendSpontaneousPaymentRequ
             node_id: self.node_id.wire2api(),
             amount_msat: self.amount_msat.wire2api(),
             extra_tlvs: self.extra_tlvs.wire2api(),
+            label: self.label.wire2api(),
         }
     }
 }
@@ -1221,6 +1224,7 @@ pub struct wire_LnUrlPayRequest {
     data: wire_LnUrlPayRequestData,
     amount_msat: u64,
     comment: *mut wire_uint_8_list,
+    payment_label: *mut wire_uint_8_list,
 }
 
 #[repr(C)]
@@ -1381,6 +1385,7 @@ pub struct wire_SendOnchainRequest {
 pub struct wire_SendPaymentRequest {
     bolt11: *mut wire_uint_8_list,
     amount_msat: *mut u64,
+    label: *mut wire_uint_8_list,
 }
 
 #[repr(C)]
@@ -1389,6 +1394,7 @@ pub struct wire_SendSpontaneousPaymentRequest {
     node_id: *mut wire_uint_8_list,
     amount_msat: u64,
     extra_tlvs: *mut wire_list_tlv_entry,
+    label: *mut wire_uint_8_list,
 }
 
 #[repr(C)]
@@ -1623,6 +1629,7 @@ impl NewWithNullPtr for wire_LnUrlPayRequest {
             data: Default::default(),
             amount_msat: Default::default(),
             comment: core::ptr::null_mut(),
+            payment_label: core::ptr::null_mut(),
         }
     }
 }
@@ -1983,6 +1990,7 @@ impl NewWithNullPtr for wire_SendPaymentRequest {
         Self {
             bolt11: core::ptr::null_mut(),
             amount_msat: core::ptr::null_mut(),
+            label: core::ptr::null_mut(),
         }
     }
 }
@@ -1999,6 +2007,7 @@ impl NewWithNullPtr for wire_SendSpontaneousPaymentRequest {
             node_id: core::ptr::null_mut(),
             amount_msat: Default::default(),
             extra_tlvs: core::ptr::null_mut(),
+            label: core::ptr::null_mut(),
         }
     }
 }
diff --git a/libs/sdk-core/src/bridge_generated.rs b/libs/sdk-core/src/bridge_generated.rs
index 4f398314f..c96b01b5a 100644
--- a/libs/sdk-core/src/bridge_generated.rs
+++ b/libs/sdk-core/src/bridge_generated.rs
@@ -1508,7 +1508,7 @@ impl rust2dart::IntoIntoDart<LnUrlPayResult> for LnUrlPayResult {
 impl support::IntoDart for LnUrlPaySuccessData {
     fn into_dart(self) -> support::DartAbi {
         vec![
-            self.payment_hash.into_into_dart().into_dart(),
+            self.payment.into_into_dart().into_dart(),
             self.success_action.into_dart(),
         ]
         .into_dart()
diff --git a/libs/sdk-core/src/greenlight/node_api.rs b/libs/sdk-core/src/greenlight/node_api.rs
index 71ac5f584..64bfe65a4 100644
--- a/libs/sdk-core/src/greenlight/node_api.rs
+++ b/libs/sdk-core/src/greenlight/node_api.rs
@@ -1059,7 +1059,12 @@ impl NodeAPI for Greenlight {
         })
     }
 
-    async fn send_payment(&self, bolt11: String, amount_msat: Option<u64>) -> NodeResult<Payment> {
+    async fn send_payment(
+        &self,
+        bolt11: String,
+        amount_msat: Option<u64>,
+        label: Option<String>,
+    ) -> NodeResult<Payment> {
         let mut description = None;
         if !bolt11.is_empty() {
             let invoice = parse_invoice(&bolt11)?;
@@ -1073,7 +1078,7 @@ impl NodeAPI for Greenlight {
             amount_msat: amount_msat.map(|amt| cln::Amount { msat: amt }),
             maxfeepercent: Some(self.sdk_config.maxfee_percent),
             retry_for: Some(self.sdk_config.payment_timeout_sec),
-            label: None,
+            label,
             maxdelay: None,
             riskfactor: None,
             localinvreqid: None,
@@ -1097,15 +1102,16 @@ impl NodeAPI for Greenlight {
         node_id: String,
         amount_msat: u64,
         extra_tlvs: Option<Vec<TlvEntry>>,
+        label: Option<String>,
     ) -> NodeResult<Payment> {
         let mut client: node::ClnClient = self.get_node_client().await?;
         let request = cln::KeysendRequest {
             destination: hex::decode(node_id)?,
             amount_msat: Some(cln::Amount { msat: amount_msat }),
-            label: Some(format!(
+            label: label.or(Some(format!(
                 "breez-{}",
                 SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis()
-            )),
+            ))),
             extratlvs: extra_tlvs.map(|tlvs| cln::TlvStream {
                 entries: tlvs
                     .into_iter()
@@ -1997,7 +2003,7 @@ impl TryFrom<cln::ListpaysPays> for Payment {
             details: PaymentDetails::Ln {
                 data: LnPaymentDetails {
                     payment_hash: hex::encode(payment.payment_hash),
-                    label: "".to_string(),
+                    label: payment.label.unwrap_or_default(),
                     destination_pubkey: payment.destination.map(hex::encode).unwrap_or_default(),
                     payment_preimage: payment.preimage.map(hex::encode).unwrap_or_default(),
                     keysend: payment.bolt11.is_none(),
diff --git a/libs/sdk-core/src/lnurl/pay.rs b/libs/sdk-core/src/lnurl/pay.rs
index fe86364f6..304061767 100644
--- a/libs/sdk-core/src/lnurl/pay.rs
+++ b/libs/sdk-core/src/lnurl/pay.rs
@@ -121,7 +121,7 @@ fn validate_invoice(user_amount_msat: u64, bolt11: &str, network: Network) -> Ln
 pub(crate) mod model {
     use crate::lnurl::error::{LnUrlError, LnUrlResult};
     use crate::lnurl::pay::{Aes256CbcDec, Aes256CbcEnc};
-    use crate::{ensure_sdk, input_parser::*};
+    use crate::{ensure_sdk, input_parser::*, Payment};
 
     use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
     use anyhow::{anyhow, Result};
@@ -144,6 +144,7 @@ pub(crate) mod model {
     /// * `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 },
@@ -158,7 +159,7 @@ pub(crate) mod model {
 
     #[derive(Serialize, Deserialize, Debug)]
     pub struct LnUrlPaySuccessData {
-        pub payment_hash: String,
+        pub payment: Payment,
         pub success_action: Option<SuccessActionProcessed>,
     }
 
@@ -873,8 +874,8 @@ mod tests {
 
     #[tokio::test]
     async fn test_lnurl_pay_no_success_action() -> Result<()> {
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
         let inv = rand_invoice_with_description_hash(temp_desc)?;
         let user_amount_msat = inv.amount_milli_satoshis().unwrap();
@@ -893,6 +894,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await?
         {
@@ -914,13 +916,13 @@ mod tests {
         }
     }
 
-    static COMMENT_LENGHT: u16 = 10;
+    static COMMENT_LENGTH: u16 = 10;
 
     #[tokio::test]
     async fn test_lnurl_pay_unsupported_success_action() -> Result<()> {
         let user_amount_msat = 11000;
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let _m =
             mock_lnurl_pay_callback_endpoint_unsupported_success_action(LnurlPayCallbackParams {
                 pay_req: &pay_req,
@@ -936,6 +938,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await;
         // An unsupported Success Action results in an error
@@ -946,8 +949,8 @@ mod tests {
 
     #[tokio::test]
     async fn test_lnurl_pay_success_payment_hash() -> Result<()> {
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
         let inv = rand_invoice_with_description_hash(temp_desc)?;
         let user_amount_msat = inv.amount_milli_satoshis().unwrap();
@@ -965,10 +968,11 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await?
         {
-            LnUrlPayResult::EndpointSuccess { data } => match data.payment_hash {
+            LnUrlPayResult::EndpointSuccess { data } => match data.payment.id {
                 s if s == inv.payment_hash().to_hex() => Ok(()),
                 _ => Err(anyhow!("Unexpected payment hash")),
             },
@@ -978,8 +982,8 @@ mod tests {
 
     #[tokio::test]
     async fn test_lnurl_pay_msg_success_action() -> Result<()> {
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
         let inv = rand_invoice_with_description_hash(temp_desc)?;
         let user_amount_msat = inv.amount_milli_satoshis().unwrap();
@@ -997,6 +1001,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await?
         {
@@ -1025,8 +1030,8 @@ mod tests {
 
     #[tokio::test]
     async fn test_lnurl_pay_msg_success_action_incorrect_amount() -> Result<()> {
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
         let inv = rand_invoice_with_description_hash(temp_desc)?;
         let user_amount_msat = inv.amount_milli_satoshis().unwrap() + 1000;
@@ -1043,7 +1048,8 @@ mod tests {
             .lnurl_pay(LnUrlPayRequest {
                 data: pay_req,
                 amount_msat: user_amount_msat,
-                comment: Some(comment)
+                comment: Some(comment),
+                payment_label: None,
             })
             .await
             .is_err());
@@ -1053,8 +1059,8 @@ mod tests {
 
     #[tokio::test]
     async fn test_lnurl_pay_msg_success_action_error_from_endpoint() -> Result<()> {
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
         let inv = rand_invoice_with_description_hash(temp_desc)?;
         let user_amount_msat = inv.amount_milli_satoshis().unwrap();
@@ -1073,6 +1079,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await;
         assert!(matches!(res, Ok(LnUrlPayResult::EndpointError { data: _ })));
@@ -1090,8 +1097,8 @@ mod tests {
 
     #[tokio::test]
     async fn test_lnurl_pay_url_success_action() -> Result<()> {
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
         let inv = rand_invoice_with_description_hash(temp_desc)?;
         let user_amount_msat = inv.amount_milli_satoshis().unwrap();
@@ -1109,6 +1116,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await?
         {
@@ -1157,8 +1165,8 @@ mod tests {
         // Generate preimage
         let preimage = sha256::Hash::hash(&rand_vec_u8(10));
 
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
 
         // The invoice (served by LNURL-pay endpoint, matching preimage and description hash)
@@ -1193,6 +1201,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await?
         {
@@ -1233,8 +1242,8 @@ mod tests {
         // Generate preimage
         let preimage = sha256::Hash::hash(&rand_vec_u8(10));
 
-        let comment = rand_string(COMMENT_LENGHT as usize);
-        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGHT);
+        let comment = rand_string(COMMENT_LENGTH as usize);
+        let pay_req = get_test_pay_req_data(0, 100_000, COMMENT_LENGTH);
         let temp_desc = pay_req.metadata_str.clone();
 
         // The invoice (served by LNURL-pay endpoint, matching preimage and description hash)
@@ -1276,6 +1285,7 @@ mod tests {
                 data: pay_req,
                 amount_msat: user_amount_msat,
                 comment: Some(comment),
+                payment_label: None,
             })
             .await?
         {
diff --git a/libs/sdk-core/src/models.rs b/libs/sdk-core/src/models.rs
index 7285b8f12..1ed8a78a3 100644
--- a/libs/sdk-core/src/models.rs
+++ b/libs/sdk-core/src/models.rs
@@ -875,6 +875,8 @@ pub struct SendPaymentRequest {
     pub bolt11: String,
     /// The amount to pay in millisatoshis. Should only be set when `bolt11` is a zero-amount invoice.
     pub amount_msat: Option<u64>,
+    /// The external label or identifier of the [Payment]
+    pub label: Option<String>,
 }
 
 /// Represents a TLV entry for a keysend payment.
@@ -895,6 +897,8 @@ pub struct SendSpontaneousPaymentRequest {
     pub amount_msat: u64,
     // Optional extra TLVs
     pub extra_tlvs: Option<Vec<TlvEntry>>,
+    /// The external label or identifier of the [Payment]
+    pub label: Option<String>,
 }
 
 /// Represents a send payment response.
@@ -1554,6 +1558,8 @@ pub struct LnUrlPayRequest {
     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.
diff --git a/libs/sdk-core/src/node_api.rs b/libs/sdk-core/src/node_api.rs
index 251bcba75..0aa102ba8 100644
--- a/libs/sdk-core/src/node_api.rs
+++ b/libs/sdk-core/src/node_api.rs
@@ -85,12 +85,18 @@ pub trait NodeAPI: Send + Sync {
         balance_changed: bool,
     ) -> NodeResult<SyncResponse>;
     /// As per the `pb::PayRequest` docs, `amount_msat` is only needed when the invoice doesn't specify an amount
-    async fn send_payment(&self, bolt11: String, amount_msat: Option<u64>) -> NodeResult<Payment>;
+    async fn send_payment(
+        &self,
+        bolt11: String,
+        amount_msat: Option<u64>,
+        label: Option<String>,
+    ) -> NodeResult<Payment>;
     async fn send_spontaneous_payment(
         &self,
         node_id: String,
         amount_msat: u64,
         extra_tlvs: Option<Vec<TlvEntry>>,
+        label: Option<String>,
     ) -> NodeResult<Payment>;
     async fn start(&self) -> NodeResult<String>;
 
diff --git a/libs/sdk-flutter/ios/Classes/bridge_generated.h b/libs/sdk-flutter/ios/Classes/bridge_generated.h
index 3ae6f68d6..87780ab9d 100644
--- a/libs/sdk-flutter/ios/Classes/bridge_generated.h
+++ b/libs/sdk-flutter/ios/Classes/bridge_generated.h
@@ -111,6 +111,7 @@ typedef struct wire_ListPaymentsRequest {
 typedef struct wire_SendPaymentRequest {
   struct wire_uint_8_list *bolt11;
   uint64_t *amount_msat;
+  struct wire_uint_8_list *label;
 } wire_SendPaymentRequest;
 
 typedef struct wire_TlvEntry {
@@ -127,6 +128,7 @@ typedef struct wire_SendSpontaneousPaymentRequest {
   struct wire_uint_8_list *node_id;
   uint64_t amount_msat;
   struct wire_list_tlv_entry *extra_tlvs;
+  struct wire_uint_8_list *label;
 } wire_SendSpontaneousPaymentRequest;
 
 typedef struct wire_OpeningFeeParams {
@@ -162,6 +164,7 @@ typedef struct wire_LnUrlPayRequest {
   struct wire_LnUrlPayRequestData data;
   uint64_t amount_msat;
   struct wire_uint_8_list *comment;
+  struct wire_uint_8_list *payment_label;
 } wire_LnUrlPayRequest;
 
 typedef struct wire_LnUrlWithdrawRequestData {
diff --git a/libs/sdk-flutter/lib/bridge_generated.dart b/libs/sdk-flutter/lib/bridge_generated.dart
index 9b199cef3..795e9f84b 100644
--- a/libs/sdk-flutter/lib/bridge_generated.dart
+++ b/libs/sdk-flutter/lib/bridge_generated.dart
@@ -890,10 +890,14 @@ class LnUrlPayRequest {
   /// An optional comment for this payment
   final String? comment;
 
+  /// The external label or identifier of the [Payment]
+  final String? paymentLabel;
+
   const LnUrlPayRequest({
     required this.data,
     required this.amountMsat,
     this.comment,
+    this.paymentLabel,
   });
 }
 
@@ -954,11 +958,11 @@ sealed class LnUrlPayResult with _$LnUrlPayResult {
 }
 
 class LnUrlPaySuccessData {
-  final String paymentHash;
+  final Payment payment;
   final SuccessActionProcessed? successAction;
 
   const LnUrlPaySuccessData({
-    required this.paymentHash,
+    required this.payment,
     this.successAction,
   });
 }
@@ -1773,9 +1777,13 @@ class SendPaymentRequest {
   /// The amount to pay in millisatoshis. Should only be set when `bolt11` is a zero-amount invoice.
   final int? amountMsat;
 
+  /// The external label or identifier of the [Payment]
+  final String? label;
+
   const SendPaymentRequest({
     required this.bolt11,
     this.amountMsat,
+    this.label,
   });
 }
 
@@ -1797,10 +1805,14 @@ class SendSpontaneousPaymentRequest {
   final int amountMsat;
   final List<TlvEntry>? extraTlvs;
 
+  /// The external label or identifier of the [Payment]
+  final String? label;
+
   const SendSpontaneousPaymentRequest({
     required this.nodeId,
     required this.amountMsat,
     this.extraTlvs,
+    this.label,
   });
 }
 
@@ -3630,7 +3642,7 @@ class BreezSdkCoreImpl implements BreezSdkCore {
     final arr = raw as List<dynamic>;
     if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
     return LnUrlPaySuccessData(
-      paymentHash: _wire2api_String(arr[0]),
+      payment: _wire2api_payment(arr[0]),
       successAction: _wire2api_opt_box_autoadd_success_action_processed(arr[1]),
     );
   }
@@ -4849,6 +4861,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase<BreezSdkCoreWire> {
     _api_fill_to_wire_ln_url_pay_request_data(apiObj.data, wireObj.data);
     wireObj.amount_msat = api2wire_u64(apiObj.amountMsat);
     wireObj.comment = api2wire_opt_String(apiObj.comment);
+    wireObj.payment_label = api2wire_opt_String(apiObj.paymentLabel);
   }
 
   void _api_fill_to_wire_ln_url_pay_request_data(
@@ -5004,6 +5017,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase<BreezSdkCoreWire> {
   void _api_fill_to_wire_send_payment_request(SendPaymentRequest apiObj, wire_SendPaymentRequest wireObj) {
     wireObj.bolt11 = api2wire_String(apiObj.bolt11);
     wireObj.amount_msat = api2wire_opt_box_autoadd_u64(apiObj.amountMsat);
+    wireObj.label = api2wire_opt_String(apiObj.label);
   }
 
   void _api_fill_to_wire_send_spontaneous_payment_request(
@@ -5011,6 +5025,7 @@ class BreezSdkCorePlatform extends FlutterRustBridgeBase<BreezSdkCoreWire> {
     wireObj.node_id = api2wire_String(apiObj.nodeId);
     wireObj.amount_msat = api2wire_u64(apiObj.amountMsat);
     wireObj.extra_tlvs = api2wire_opt_list_tlv_entry(apiObj.extraTlvs);
+    wireObj.label = api2wire_opt_String(apiObj.label);
   }
 
   void _api_fill_to_wire_sign_message_request(SignMessageRequest apiObj, wire_SignMessageRequest wireObj) {
@@ -6547,6 +6562,8 @@ final class wire_SendPaymentRequest extends ffi.Struct {
   external ffi.Pointer<wire_uint_8_list> bolt11;
 
   external ffi.Pointer<ffi.Uint64> amount_msat;
+
+  external ffi.Pointer<wire_uint_8_list> label;
 }
 
 final class wire_TlvEntry extends ffi.Struct {
@@ -6570,6 +6587,8 @@ final class wire_SendSpontaneousPaymentRequest extends ffi.Struct {
   external int amount_msat;
 
   external ffi.Pointer<wire_list_tlv_entry> extra_tlvs;
+
+  external ffi.Pointer<wire_uint_8_list> label;
 }
 
 final class wire_OpeningFeeParams extends ffi.Struct {
@@ -6633,6 +6652,8 @@ final class wire_LnUrlPayRequest extends ffi.Struct {
   external int amount_msat;
 
   external ffi.Pointer<wire_uint_8_list> comment;
+
+  external ffi.Pointer<wire_uint_8_list> payment_label;
 }
 
 final class wire_LnUrlWithdrawRequestData extends ffi.Struct {
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 042e6c44e..1b8296a5d 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
@@ -1155,10 +1155,12 @@ fun asLnUrlPayRequest(lnUrlPayRequest: ReadableMap): LnUrlPayRequest? {
     val data = lnUrlPayRequest.getMap("data")?.let { asLnUrlPayRequestData(it) }!!
     val amountMsat = lnUrlPayRequest.getDouble("amountMsat").toULong()
     val comment = if (hasNonNullKey(lnUrlPayRequest, "comment")) lnUrlPayRequest.getString("comment") else null
+    val paymentLabel = if (hasNonNullKey(lnUrlPayRequest, "paymentLabel")) lnUrlPayRequest.getString("paymentLabel") else null
     return LnUrlPayRequest(
         data,
         amountMsat,
         comment,
+        paymentLabel,
     )
 }
 
@@ -1167,6 +1169,7 @@ fun readableMapOf(lnUrlPayRequest: LnUrlPayRequest): ReadableMap {
         "data" to readableMapOf(lnUrlPayRequest.data),
         "amountMsat" to lnUrlPayRequest.amountMsat,
         "comment" to lnUrlPayRequest.comment,
+        "paymentLabel" to lnUrlPayRequest.paymentLabel,
     )
 }
 
@@ -1241,7 +1244,7 @@ fun asLnUrlPaySuccessData(lnUrlPaySuccessData: ReadableMap): LnUrlPaySuccessData
     if (!validateMandatoryFields(
             lnUrlPaySuccessData,
             arrayOf(
-                "paymentHash",
+                "payment",
             ),
         )
     ) {
@@ -1255,17 +1258,17 @@ fun asLnUrlPaySuccessData(lnUrlPaySuccessData: ReadableMap): LnUrlPaySuccessData
         } else {
             null
         }
-    val paymentHash = lnUrlPaySuccessData.getString("paymentHash")!!
+    val payment = lnUrlPaySuccessData.getMap("payment")?.let { asPayment(it) }!!
     return LnUrlPaySuccessData(
         successAction,
-        paymentHash,
+        payment,
     )
 }
 
 fun readableMapOf(lnUrlPaySuccessData: LnUrlPaySuccessData): ReadableMap {
     return readableMapOf(
         "successAction" to lnUrlPaySuccessData.successAction?.let { readableMapOf(it) },
-        "paymentHash" to lnUrlPaySuccessData.paymentHash,
+        "payment" to readableMapOf(lnUrlPaySuccessData.payment),
     )
 }
 
@@ -3217,9 +3220,11 @@ fun asSendPaymentRequest(sendPaymentRequest: ReadableMap): SendPaymentRequest? {
     }
     val bolt11 = sendPaymentRequest.getString("bolt11")!!
     val amountMsat = if (hasNonNullKey(sendPaymentRequest, "amountMsat")) sendPaymentRequest.getDouble("amountMsat").toULong() else null
+    val label = if (hasNonNullKey(sendPaymentRequest, "label")) sendPaymentRequest.getString("label") else null
     return SendPaymentRequest(
         bolt11,
         amountMsat,
+        label,
     )
 }
 
@@ -3227,6 +3232,7 @@ fun readableMapOf(sendPaymentRequest: SendPaymentRequest): ReadableMap {
     return readableMapOf(
         "bolt11" to sendPaymentRequest.bolt11,
         "amountMsat" to sendPaymentRequest.amountMsat,
+        "label" to sendPaymentRequest.label,
     )
 }
 
@@ -3299,10 +3305,12 @@ fun asSendSpontaneousPaymentRequest(sendSpontaneousPaymentRequest: ReadableMap):
         } else {
             null
         }
+    val label = if (hasNonNullKey(sendSpontaneousPaymentRequest, "label")) sendSpontaneousPaymentRequest.getString("label") else null
     return SendSpontaneousPaymentRequest(
         nodeId,
         amountMsat,
         extraTlvs,
+        label,
     )
 }
 
@@ -3311,6 +3319,7 @@ fun readableMapOf(sendSpontaneousPaymentRequest: SendSpontaneousPaymentRequest):
         "nodeId" to sendSpontaneousPaymentRequest.nodeId,
         "amountMsat" to sendSpontaneousPaymentRequest.amountMsat,
         "extraTlvs" to sendSpontaneousPaymentRequest.extraTlvs?.let { readableArrayOf(it) },
+        "label" to sendSpontaneousPaymentRequest.label,
     )
 }
 
diff --git a/libs/sdk-react-native/ios/BreezSDKMapper.swift b/libs/sdk-react-native/ios/BreezSDKMapper.swift
index 34e0c0a2b..94bab80e5 100644
--- a/libs/sdk-react-native/ios/BreezSDKMapper.swift
+++ b/libs/sdk-react-native/ios/BreezSDKMapper.swift
@@ -1278,11 +1278,19 @@ enum BreezSDKMapper {
             }
             comment = commentTmp
         }
+        var paymentLabel: String?
+        if hasNonNilKey(data: lnUrlPayRequest, key: "paymentLabel") {
+            guard let paymentLabelTmp = lnUrlPayRequest["paymentLabel"] as? String else {
+                throw SdkError.Generic(message: errUnexpectedValue(fieldName: "paymentLabel"))
+            }
+            paymentLabel = paymentLabelTmp
+        }
 
         return LnUrlPayRequest(
             data: data,
             amountMsat: amountMsat,
-            comment: comment
+            comment: comment,
+            paymentLabel: paymentLabel
         )
     }
 
@@ -1291,6 +1299,7 @@ enum BreezSDKMapper {
             "data": dictionaryOf(lnUrlPayRequestData: lnUrlPayRequest.data),
             "amountMsat": lnUrlPayRequest.amountMsat,
             "comment": lnUrlPayRequest.comment == nil ? nil : lnUrlPayRequest.comment,
+            "paymentLabel": lnUrlPayRequest.paymentLabel == nil ? nil : lnUrlPayRequest.paymentLabel,
         ]
     }
 
@@ -1384,20 +1393,21 @@ enum BreezSDKMapper {
             successAction = try asSuccessActionProcessed(successActionProcessed: successActionTmp)
         }
 
-        guard let paymentHash = lnUrlPaySuccessData["paymentHash"] as? String else {
-            throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentHash", typeName: "LnUrlPaySuccessData"))
+        guard let paymentTmp = lnUrlPaySuccessData["payment"] as? [String: Any?] else {
+            throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "payment", typeName: "LnUrlPaySuccessData"))
         }
+        let payment = try asPayment(payment: paymentTmp)
 
         return LnUrlPaySuccessData(
             successAction: successAction,
-            paymentHash: paymentHash
+            payment: payment
         )
     }
 
     static func dictionaryOf(lnUrlPaySuccessData: LnUrlPaySuccessData) -> [String: Any?] {
         return [
             "successAction": lnUrlPaySuccessData.successAction == nil ? nil : dictionaryOf(successActionProcessed: lnUrlPaySuccessData.successAction!),
-            "paymentHash": lnUrlPaySuccessData.paymentHash,
+            "payment": dictionaryOf(payment: lnUrlPaySuccessData.payment),
         ]
     }
 
@@ -3505,10 +3515,18 @@ enum BreezSDKMapper {
             }
             amountMsat = amountMsatTmp
         }
+        var label: String?
+        if hasNonNilKey(data: sendPaymentRequest, key: "label") {
+            guard let labelTmp = sendPaymentRequest["label"] as? String else {
+                throw SdkError.Generic(message: errUnexpectedValue(fieldName: "label"))
+            }
+            label = labelTmp
+        }
 
         return SendPaymentRequest(
             bolt11: bolt11,
-            amountMsat: amountMsat
+            amountMsat: amountMsat,
+            label: label
         )
     }
 
@@ -3516,6 +3534,7 @@ enum BreezSDKMapper {
         return [
             "bolt11": sendPaymentRequest.bolt11,
             "amountMsat": sendPaymentRequest.amountMsat == nil ? nil : sendPaymentRequest.amountMsat,
+            "label": sendPaymentRequest.label == nil ? nil : sendPaymentRequest.label,
         ]
     }
 
@@ -3581,10 +3600,19 @@ enum BreezSDKMapper {
             extraTlvs = try asTlvEntryList(arr: extraTlvsTmp)
         }
 
+        var label: String?
+        if hasNonNilKey(data: sendSpontaneousPaymentRequest, key: "label") {
+            guard let labelTmp = sendSpontaneousPaymentRequest["label"] as? String else {
+                throw SdkError.Generic(message: errUnexpectedValue(fieldName: "label"))
+            }
+            label = labelTmp
+        }
+
         return SendSpontaneousPaymentRequest(
             nodeId: nodeId,
             amountMsat: amountMsat,
-            extraTlvs: extraTlvs
+            extraTlvs: extraTlvs,
+            label: label
         )
     }
 
@@ -3593,6 +3621,7 @@ enum BreezSDKMapper {
             "nodeId": sendSpontaneousPaymentRequest.nodeId,
             "amountMsat": sendSpontaneousPaymentRequest.amountMsat,
             "extraTlvs": sendSpontaneousPaymentRequest.extraTlvs == nil ? nil : arrayOf(tlvEntryList: sendSpontaneousPaymentRequest.extraTlvs!),
+            "label": sendSpontaneousPaymentRequest.label == nil ? nil : sendSpontaneousPaymentRequest.label,
         ]
     }
 
diff --git a/libs/sdk-react-native/src/index.ts b/libs/sdk-react-native/src/index.ts
index cf26e1aa0..045e9cd7c 100644
--- a/libs/sdk-react-native/src/index.ts
+++ b/libs/sdk-react-native/src/index.ts
@@ -186,6 +186,7 @@ export type LnUrlPayRequest = {
     data: LnUrlPayRequestData
     amountMsat: number
     comment?: string
+    paymentLabel?: string
 }
 
 export type LnUrlPayRequestData = {
@@ -200,7 +201,7 @@ export type LnUrlPayRequestData = {
 
 export type LnUrlPaySuccessData = {
     successAction?: SuccessActionProcessed
-    paymentHash: string
+    payment: Payment
 }
 
 export type LnUrlWithdrawRequest = {
@@ -486,6 +487,7 @@ export type SendOnchainResponse = {
 export type SendPaymentRequest = {
     bolt11: string
     amountMsat?: number
+    label?: string
 }
 
 export type SendPaymentResponse = {
@@ -496,6 +498,7 @@ export type SendSpontaneousPaymentRequest = {
     nodeId: string
     amountMsat: number
     extraTlvs?: TlvEntry[]
+    label?: string
 }
 
 export type ServiceHealthCheckResponse = {
diff --git a/tools/sdk-cli/src/command_handlers.rs b/tools/sdk-cli/src/command_handlers.rs
index 07ef89477..f81c24a9f 100644
--- a/tools/sdk-cli/src/command_handlers.rs
+++ b/tools/sdk-cli/src/command_handlers.rs
@@ -242,11 +242,13 @@ pub(crate) async fn handle_command(
         Commands::SendPayment {
             bolt11,
             amount_msat,
+            label,
         } => {
             let payment = sdk()?
                 .send_payment(SendPaymentRequest {
                     bolt11,
                     amount_msat,
+                    label,
                 })
                 .await?;
             serde_json::to_string_pretty(&payment).map_err(|e| e.into())
@@ -254,12 +256,14 @@ pub(crate) async fn handle_command(
         Commands::SendSpontaneousPayment {
             node_id,
             amount_msat,
+            label,
         } => {
             let response = sdk()?
                 .send_spontaneous_payment(SendSpontaneousPaymentRequest {
                     node_id,
                     amount_msat,
                     extra_tlvs: None,
+                    label,
                 })
                 .await?;
             serde_json::to_string_pretty(&response.payment).map_err(|e| e.into())
@@ -460,7 +464,7 @@ pub(crate) async fn handle_command(
             let res = sdk()?.check_message(req).await?;
             Ok(format!("Message was signed by node: {}", res.is_valid))
         }
-        Commands::LnurlPay { lnurl } => match parse(&lnurl).await? {
+        Commands::LnurlPay { lnurl, label } => match parse(&lnurl).await? {
             LnUrlPay { data: pd } => {
                 let prompt = format!(
                     "Amount to pay in millisatoshi (min {} msat, max {} msat: ",
@@ -473,6 +477,7 @@ pub(crate) async fn handle_command(
                         data: pd,
                         amount_msat: amount_msat.parse::<u64>()?,
                         comment: None,
+                        payment_label: label,
                     })
                     .await?;
                 //show_results(pay_res);
diff --git a/tools/sdk-cli/src/commands.rs b/tools/sdk-cli/src/commands.rs
index bc25ef416..d9a4fa20f 100644
--- a/tools/sdk-cli/src/commands.rs
+++ b/tools/sdk-cli/src/commands.rs
@@ -46,12 +46,20 @@ pub(crate) enum Commands {
 
         #[clap(name = "amount_msat", short = 'a', long = "amt")]
         amount_msat: Option<u64>,
+
+        /// The external label or identifier of the payment
+        #[clap(name = "label", short = 'l', long = "label")]
+        label: Option<String>,
     },
 
     /// [pay] Send a spontaneous (keysend) payment
     SendSpontaneousPayment {
         node_id: String,
         amount_msat: u64,
+
+        /// The external label or identifier of the payment
+        #[clap(name = "label", short = 'l', long = "label")]
+        label: Option<String>,
     },
 
     /// [pay] Generate a bolt11 invoice
@@ -81,6 +89,10 @@ pub(crate) enum Commands {
     /// [lnurl] Pay using lnurl pay
     LnurlPay {
         lnurl: String,
+
+        /// The external label or identifier of the payment
+        #[clap(name = "label", short = 'l', long = "label")]
+        label: Option<String>,
     },
 
     /// [lnurl] Withdraw using lnurl withdraw


Test patch:
diff --git a/libs/sdk-core/src/test_utils.rs b/libs/sdk-core/src/test_utils.rs
index abbd430ab..39984912b 100644
--- a/libs/sdk-core/src/test_utils.rs
+++ b/libs/sdk-core/src/test_utils.rs
@@ -372,7 +372,12 @@ impl NodeAPI for MockNodeAPI {
         Err(NodeError::Generic(anyhow!("Not implemented")))
     }
 
-    async fn send_payment(&self, bolt11: String, _amount_msat: Option<u64>) -> NodeResult<Payment> {
+    async fn send_payment(
+        &self,
+        bolt11: String,
+        _amount_msat: Option<u64>,
+        _label: Option<String>,
+    ) -> NodeResult<Payment> {
         let payment = self.add_dummy_payment_for(bolt11, None, None).await?;
         Ok(payment)
     }
@@ -382,6 +387,7 @@ impl NodeAPI for MockNodeAPI {
         _node_id: String,
         _amount_msat: u64,
         _extra_tlvs: Option<Vec<TlvEntry>>,
+        _label: Option<String>,
     ) -> NodeResult<Payment> {
         let payment = self.add_dummy_payment_rand().await?;
         Ok(payment)