From 8040fcc97afcb5e9aed85ac5ff18fd224f06a6b7 Mon Sep 17 00:00:00 2001 From: anon Date: Fri, 20 Mar 2026 09:38:53 +0100 Subject: [PATCH] feat: min_cltv_expiry_delta for hold invoices --- bindings/ldk_node.udl | 4 ++ src/payment/bolt11.rs | 81 ++++++++++++++++++++++++++++++++++++--- src/payment/unified_qr.rs | 1 + tests/common/mod.rs | 35 +++++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c205c35cf..33c9d98c8 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -209,10 +209,14 @@ interface Bolt11Payment { [Throws=NodeError] Bolt11Invoice receive_for_hash(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash); [Throws=NodeError] + Bolt11Invoice receive_for_hash_with_min_cltv_expiry_delta(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash, u16 min_cltv_expiry_delta); + [Throws=NodeError] Bolt11Invoice receive_variable_amount([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs); [Throws=NodeError] Bolt11Invoice receive_variable_amount_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash); [Throws=NodeError] + Bolt11Invoice receive_variable_amount_for_hash_with_min_cltv_expiry_delta([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, PaymentHash payment_hash, u16 min_cltv_expiry_delta); + [Throws=NodeError] Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); [Throws=NodeError] Bolt11Invoice receive_via_jit_channel_for_hash(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat, PaymentHash payment_hash); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index eda349774..826b45248 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -412,7 +412,8 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner(Some(amount_msat), &description, expiry_secs, None)?; + let invoice = + self.receive_inner(Some(amount_msat), &description, expiry_secs, None, None)?; Ok(maybe_wrap(invoice)) } @@ -435,8 +436,44 @@ impl Bolt11Payment { payment_hash: PaymentHash, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = - self.receive_inner(Some(amount_msat), &description, expiry_secs, Some(payment_hash))?; + let invoice = self.receive_inner( + Some(amount_msat), + &description, + expiry_secs, + Some(payment_hash), + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a payment of the amount + /// given for the given payment hash. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. + /// + /// `min_cltv_expiry_delta` sets the minimum CLTV delta to use for the final hop. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + pub fn receive_for_hash_with_min_cltv_expiry_delta( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: PaymentHash, min_cltv_expiry_delta: u16, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner( + Some(amount_msat), + &description, + expiry_secs, + Some(payment_hash), + Some(min_cltv_expiry_delta), + )?; Ok(maybe_wrap(invoice)) } @@ -448,7 +485,7 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner(None, &description, expiry_secs, None)?; + let invoice = self.receive_inner(None, &description, expiry_secs, None, None)?; Ok(maybe_wrap(invoice)) } @@ -470,19 +507,53 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_inner(None, &description, expiry_secs, Some(payment_hash))?; + let invoice = + self.receive_inner(None, &description, expiry_secs, Some(payment_hash), None)?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a payment for the given payment hash + /// and the amount to be determined by the user, also known as a "zero-amount" invoice. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. + /// + /// `min_cltv_expiry_delta` sets the minimum CLTV delta to use for the final hop. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + pub fn receive_variable_amount_for_hash_with_min_cltv_expiry_delta( + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, + min_cltv_expiry_delta: u16, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner( + None, + &description, + expiry_secs, + Some(payment_hash), + Some(min_cltv_expiry_delta), + )?; Ok(maybe_wrap(invoice)) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, + min_cltv_expiry_delta: Option, ) -> Result { let invoice = { let invoice_params = Bolt11InvoiceParameters { amount_msats: amount_msat, description: invoice_description.clone(), invoice_expiry_delta_secs: Some(expiry_secs), + min_final_cltv_expiry_delta: min_cltv_expiry_delta, payment_hash: manual_claim_payment_hash, ..Default::default() }; diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 6ebf25563..298dd87dc 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -112,6 +112,7 @@ impl UnifiedQrPayment { &invoice_description, expiry_sec, None, + None, ) { Ok(invoice) => Some(invoice), Err(e) => { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 4dc0b110c..f3cbc74f2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -966,6 +966,41 @@ pub(crate) async fn do_channel_full_cycle( manual_payment_hash, ) .unwrap(); + let min_cltv_expiry_delta = 100; + let manual_preimage_with_min_cltv = PaymentPreimage([44u8; 32]); + let manual_payment_hash_with_min_cltv = + PaymentHash(Sha256::hash(&manual_preimage_with_min_cltv.0).to_byte_array()); + let manual_invoice_with_min_cltv = node_b + .bolt11_payment() + .receive_for_hash_with_min_cltv_expiry_delta( + invoice_amount_3_msat, + &invoice_description.clone().into(), + 9217, + manual_payment_hash_with_min_cltv, + min_cltv_expiry_delta, + ) + .unwrap(); + let expected_min_final_cltv_expiry_delta = u64::from(min_cltv_expiry_delta) + 3; + assert_eq!( + manual_invoice_with_min_cltv.min_final_cltv_expiry_delta(), + expected_min_final_cltv_expiry_delta + ); + let manual_variable_preimage_with_min_cltv = PaymentPreimage([45u8; 32]); + let manual_variable_payment_hash_with_min_cltv = + PaymentHash(Sha256::hash(&manual_variable_preimage_with_min_cltv.0).to_byte_array()); + let manual_variable_invoice_with_min_cltv = node_b + .bolt11_payment() + .receive_variable_amount_for_hash_with_min_cltv_expiry_delta( + &invoice_description.clone().into(), + 9217, + manual_variable_payment_hash_with_min_cltv, + min_cltv_expiry_delta, + ) + .unwrap(); + assert_eq!( + manual_variable_invoice_with_min_cltv.min_final_cltv_expiry_delta(), + expected_min_final_cltv_expiry_delta + ); let manual_payment_id = node_a.bolt11_payment().send(&manual_invoice, None).unwrap(); let claimable_amount_msat = expect_payment_claimable_event!(