Skip to content

Commit dced4c4

Browse files
committed
Support paying zero-value invoices
1 parent 376abdc commit dced4c4

File tree

1 file changed

+88
-3
lines changed

1 file changed

+88
-3
lines changed

lightning-invoice/src/payment.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,29 @@ where
196196

197197
/// Pays the given [`Invoice`], caching it for later use in case a retry is needed.
198198
pub fn pay_invoice(&self, invoice: &Invoice) -> Result<PaymentId, PaymentError> {
199+
if invoice.amount_milli_satoshis().is_none() {
200+
Err(PaymentError::Invoice("amount missing"))
201+
} else {
202+
self.pay_invoice_internal(invoice, None)
203+
}
204+
}
205+
206+
/// Pays the given zero-value [`Invoice`] using the given amount, caching it for later use in
207+
/// case a retry is needed.
208+
pub fn pay_zero_value_invoice(
209+
&self, invoice: &Invoice, amount_msats: u64
210+
) -> Result<PaymentId, PaymentError> {
211+
if invoice.amount_milli_satoshis().is_some() {
212+
Err(PaymentError::Invoice("amount unexpected"))
213+
} else {
214+
self.pay_invoice_internal(invoice, Some(amount_msats))
215+
}
216+
}
217+
218+
fn pay_invoice_internal(
219+
&self, invoice: &Invoice, amount_msats: Option<u64>
220+
) -> Result<PaymentId, PaymentError> {
221+
debug_assert!(invoice.amount_milli_satoshis().is_some() ^ amount_msats.is_some());
199222
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
200223
let mut payment_cache = self.payment_cache.lock().unwrap();
201224
match payment_cache.entry(payment_hash) {
@@ -206,11 +229,9 @@ where
206229
if let Some(features) = invoice.features() {
207230
payee = payee.with_features(features.clone());
208231
}
209-
let final_value_msat = invoice.amount_milli_satoshis()
210-
.ok_or(PaymentError::Invoice("amount missing"))?;
211232
let params = RouteParameters {
212233
payee,
213-
final_value_msat,
234+
final_value_msat: invoice.amount_milli_satoshis().or(amount_msats).unwrap(),
214235
final_cltv_expiry_delta: invoice.min_final_cltv_expiry() as u32,
215236
};
216237
let first_hops = self.payer.first_hops();
@@ -334,6 +355,21 @@ mod tests {
334355
.unwrap()
335356
}
336357

358+
fn zero_value_invoice(payment_preimage: PaymentPreimage) -> Invoice {
359+
let payment_hash = Sha256::hash(&payment_preimage.0);
360+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
361+
InvoiceBuilder::new(Currency::Bitcoin)
362+
.description("test".into())
363+
.payment_hash(payment_hash)
364+
.payment_secret(PaymentSecret([0; 32]))
365+
.current_timestamp()
366+
.min_final_cltv_expiry(144)
367+
.build_signed(|hash| {
368+
Secp256k1::new().sign_recoverable(hash, &private_key)
369+
})
370+
.unwrap()
371+
}
372+
337373
#[test]
338374
fn pays_invoice_on_first_attempt() {
339375
let event_handled = core::cell::RefCell::new(false);
@@ -631,6 +667,55 @@ mod tests {
631667
}
632668
}
633669

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

636721
impl TestRouter {

0 commit comments

Comments
 (0)