Skip to content

Commit aa16363

Browse files
committed
Support paying zero-value invoices
1 parent 1a188d5 commit aa16363

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
@@ -197,6 +197,29 @@ where
197197

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

366+
fn zero_value_invoice(payment_preimage: PaymentPreimage) -> Invoice {
367+
let payment_hash = Sha256::hash(&payment_preimage.0);
368+
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
369+
InvoiceBuilder::new(Currency::Bitcoin)
370+
.description("test".into())
371+
.payment_hash(payment_hash)
372+
.payment_secret(PaymentSecret([0; 32]))
373+
.current_timestamp()
374+
.min_final_cltv_expiry(144)
375+
.build_signed(|hash| {
376+
Secp256k1::new().sign_recoverable(hash, &private_key)
377+
})
378+
.unwrap()
379+
}
380+
345381
#[test]
346382
fn pays_invoice_on_first_attempt() {
347383
let event_handled = core::cell::RefCell::new(false);
@@ -681,6 +717,55 @@ mod tests {
681717
}
682718
}
683719

720+
#[test]
721+
fn pays_zero_value_invoice_using_amount() {
722+
let event_handled = core::cell::RefCell::new(false);
723+
let event_handler = |_: &_| { *event_handled.borrow_mut() = true; };
724+
725+
let payment_preimage = PaymentPreimage([1; 32]);
726+
let invoice = zero_value_invoice(payment_preimage);
727+
let payment_hash = PaymentHash(invoice.payment_hash().clone().into_inner());
728+
let final_value_msat = 100;
729+
730+
let payer = TestPayer::new().expect_value_msat(final_value_msat);
731+
let router = TestRouter {};
732+
let logger = TestLogger::new();
733+
let invoice_payer =
734+
InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(0));
735+
736+
let payment_id =
737+
Some(invoice_payer.pay_zero_value_invoice(&invoice, final_value_msat).unwrap());
738+
assert_eq!(*payer.attempts.borrow(), 1);
739+
740+
invoice_payer.handle_event(&Event::PaymentSent {
741+
payment_id, payment_preimage, payment_hash
742+
});
743+
assert_eq!(*event_handled.borrow(), true);
744+
assert_eq!(*payer.attempts.borrow(), 1);
745+
}
746+
747+
#[test]
748+
fn fails_paying_zero_value_invoice_with_amount() {
749+
let event_handled = core::cell::RefCell::new(false);
750+
let event_handler = |_: &_| { *event_handled.borrow_mut() = true; };
751+
752+
let payer = TestPayer::new();
753+
let router = TestRouter {};
754+
let logger = TestLogger::new();
755+
let invoice_payer =
756+
InvoicePayer::new(&payer, router, &logger, event_handler, RetryAttempts(0));
757+
758+
let payment_preimage = PaymentPreimage([1; 32]);
759+
let invoice = invoice(payment_preimage);
760+
761+
// Cannot repay an invoice pending payment.
762+
match invoice_payer.pay_zero_value_invoice(&invoice, 100) {
763+
Err(PaymentError::Invoice("amount unexpected")) => {},
764+
Err(_) => panic!("unexpected error"),
765+
Ok(_) => panic!("expected invoice error"),
766+
}
767+
}
768+
684769
struct TestRouter;
685770

686771
impl TestRouter {

0 commit comments

Comments
 (0)