Skip to content

Commit cad36ca

Browse files
committed
Support paying zero-value invoices
1 parent 76d905f commit cad36ca

File tree

1 file changed

+83
-2
lines changed

1 file changed

+83
-2
lines changed

lightning-invoice/src/payment.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,29 @@ where
207207

208208
/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
209209
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {
210+
if invoice.amount_milli_satoshis().is_none() {
211+
Err(PaymentError::Invoice("amount missing"))
212+
} else {
213+
self.pay_invoice_internal(invoice, None)
214+
}
215+
}
216+
217+
/// Pays the given zero-value [`Invoice`] using the given amount, caching it for later use in
218+
/// case a retry is needed.
219+
pub fn pay_zero_value_invoice(
220+
&self, invoice: &Invoice, amount_msats: u64
221+
) -> Result<PaymentId, PaymentError> {
222+
if invoice.amount_milli_satoshis().is_some() {
223+
Err(PaymentError::Invoice("amount unexpected"))
224+
} else {
225+
self.pay_invoice_internal(invoice, Some(amount_msats))
226+
}
227+
}
228+
229+
fn pay_invoice_internal(
230+
&self, invoice: &Invoice, amount_msats: Option<u64>
231+
) -> Result<PaymentId, PaymentError> {
232+
debug_assert!(invoice.amount_milli_satoshis().is_some() ^ amount_msats.is_some());
210233
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
211234
let mut invoice_cache = self.invoice_cache.lock().unwrap();
212235
match invoice_cache.entry(payment_hash) {
@@ -216,8 +239,7 @@ where
216239
let payee_features = invoice.features().cloned();
217240
let first_hops = self.payer.first_hops();
218241
let last_hops = invoice.route_hints();
219-
let final_value_msat = invoice.amount_milli_satoshis()
220-
.ok_or(PaymentError::Invoice("amount missing"))?;
242+
let final_value_msat = invoice.amount_milli_satoshis().or(amount_msats).unwrap();
221243
let final_cltv = invoice.min_final_cltv_expiry() as u32;
222244
let route = self.router.find_route(
223245
&payer,
@@ -353,6 +375,21 @@ mod tests {
353375
.unwrap()
354376
}
355377

378+
fn zero_value_invoice(payment_preimage: PaymentPreimage) -> Invoice {
379+
let payment_hash = Sha256::hash(&payment_preimage.0);
380+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
381+
InvoiceBuilder::new(Currency::Bitcoin)
382+
.description("test".into())
383+
.payment_hash(payment_hash)
384+
.payment_secret(PaymentSecret([0; 32]))
385+
.current_timestamp()
386+
.min_final_cltv_expiry(144)
387+
.build_signed(|hash| {
388+
Secp256k1::new().sign_recoverable(hash, &private_key)
389+
})
390+
.unwrap()
391+
}
392+
356393
#[test]
357394
fn pays_invoice_on_first_attempt() {
358395
let event_handled = core::cell::RefCell::new(false);
@@ -637,6 +674,50 @@ mod tests {
637674
}
638675
}
639676

677+
#[test]
678+
fn pays_zero_value_invoice_using_amount() {
679+
let event_handled = core::cell::RefCell::new(false);
680+
let event_handler = |_: &_| { *event_handled.borrow_mut() = true; };
681+
682+
let payment_preimage = PaymentPreimage([1; 32]);
683+
let invoice = zero_value_invoice(payment_preimage);
684+
let final_value_msat = 100;
685+
686+
let payer = TestPayer::new().expect_value_msat(final_value_msat);
687+
let router = TestRouter {};
688+
let logger = TestLogger::new();
689+
let invoice_payer =
690+
InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(0));
691+
assert!(invoice_payer.pay_zero_value_invoice(&invoice, final_value_msat).is_ok());
692+
assert_eq!(*payer.attempts.borrow(), 1);
693+
694+
invoice_payer.handle_event(&Event::PaymentSent { payment_preimage });
695+
assert_eq!(*event_handled.borrow(), true);
696+
assert_eq!(*payer.attempts.borrow(), 1);
697+
}
698+
699+
#[test]
700+
fn fails_paying_zero_value_invoice_with_amount() {
701+
let event_handled = core::cell::RefCell::new(false);
702+
let event_handler = |_: &_| { *event_handled.borrow_mut() = true; };
703+
704+
let payer = TestPayer::new();
705+
let router = TestRouter {};
706+
let logger = TestLogger::new();
707+
let invoice_payer =
708+
InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(0));
709+
710+
let payment_preimage = PaymentPreimage([1; 32]);
711+
let invoice = invoice(payment_preimage);
712+
713+
// Cannot repay an invoice pending payment.
714+
match invoice_payer.pay_zero_value_invoice(&invoice, 100) {
715+
Err(PaymentError::Invoice("amount unexpected")) => {},
716+
Err(_) => panic!("unexpected error"),
717+
Ok(_) => panic!("expected invoice error"),
718+
}
719+
}
720+
640721
struct TestRouter;
641722

642723
impl TestRouter {

0 commit comments

Comments
 (0)