Skip to content

Commit 211f84b

Browse files
benthecarmanclaude
andcommitted
Add cancel_invoice to Bolt11Payment
Allow cancelling a previously created BOLT11 invoice by payment hash. Enterprise integrators need this when they short-circuit an invoice with an internal database transfer or when an alternative payment method is used (e.g., on-chain payment via unified URI). The PaymentClaimable event handler rejects HTLCs for cancelled invoices only when the preimage is known (auto-claim payments), preserving retry behavior for manual-claim (_for_hash) payments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 804f00f commit 211f84b

File tree

3 files changed

+143
-0
lines changed

3 files changed

+143
-0
lines changed

src/event.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,19 @@ where
688688
};
689689
}
690690

691+
// If the invoice has been canceled, reject the HTLC. We only do this
692+
// when the preimage is known to preserve retry behavior for `_for_hash`
693+
// manual-claim payments, where `fail_for_hash` may have been a
694+
// temporary rejection (e.g., preimage not yet available).
695+
if info.status == PaymentStatus::Failed && purpose.preimage().is_some() {
696+
log_info!(
697+
self.logger,
698+
"Refused inbound payment with ID {payment_id}: invoice has been canceled."
699+
);
700+
self.channel_manager.fail_htlc_backwards(&payment_hash);
701+
return Ok(());
702+
}
703+
691704
if info.status == PaymentStatus::Succeeded
692705
|| matches!(info.kind, PaymentKind::Spontaneous { .. })
693706
{

src/payment/bolt11.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,65 @@ impl Bolt11Payment {
555555
Ok(())
556556
}
557557

558+
/// Allows to cancel a previously created invoice identified by the given payment hash.
559+
///
560+
/// This will mark the corresponding payment as failed and cause any incoming HTLCs for this
561+
/// invoice to be automatically failed back.
562+
///
563+
/// Will check that the payment is known and has not already been claimed, and will return an
564+
/// error otherwise.
565+
pub fn cancel_invoice(&self, payment_hash: PaymentHash) -> Result<(), Error> {
566+
let payment_id = PaymentId(payment_hash.0);
567+
568+
if let Some(info) = self.payment_store.get(&payment_id) {
569+
if info.direction != PaymentDirection::Inbound {
570+
log_error!(
571+
self.logger,
572+
"Failed to cancel invoice for non-inbound payment with hash {payment_hash}"
573+
);
574+
return Err(Error::InvalidPaymentHash);
575+
}
576+
577+
if info.status == PaymentStatus::Succeeded {
578+
log_error!(
579+
self.logger,
580+
"Failed to cancel invoice with hash {payment_hash}: payment has already been claimed",
581+
);
582+
return Err(Error::InvalidPaymentHash);
583+
}
584+
} else {
585+
log_error!(self.logger, "Failed to cancel unknown invoice with hash {payment_hash}");
586+
return Err(Error::InvalidPaymentHash);
587+
}
588+
589+
let update = PaymentDetailsUpdate {
590+
status: Some(PaymentStatus::Failed),
591+
..PaymentDetailsUpdate::new(payment_id)
592+
};
593+
594+
match self.payment_store.update(update) {
595+
Ok(DataStoreUpdateResult::Updated) | Ok(DataStoreUpdateResult::Unchanged) => (),
596+
Ok(DataStoreUpdateResult::NotFound) => {
597+
log_error!(
598+
self.logger,
599+
"Failed to cancel unknown invoice with hash {payment_hash}",
600+
);
601+
return Err(Error::InvalidPaymentHash);
602+
},
603+
Err(e) => {
604+
log_error!(self.logger, "Failed to cancel invoice with hash {payment_hash}: {e}");
605+
return Err(e);
606+
},
607+
}
608+
609+
// Fail back any HTLCs that have already arrived but whose `PaymentClaimable` event
610+
// hasn't been processed yet. This is a no-op if no HTLCs are pending for this hash.
611+
self.channel_manager.fail_htlc_backwards(&payment_hash);
612+
613+
log_info!(self.logger, "Successfully canceled invoice with hash {payment_hash}");
614+
Ok(())
615+
}
616+
558617
/// Returns a payable invoice that can be used to request and receive a payment of the amount
559618
/// given.
560619
///

tests/integration_tests_rust.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,6 +1989,77 @@ async fn spontaneous_send_with_custom_preimage() {
19891989
}
19901990
}
19911991

1992+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1993+
async fn cancel_invoice() {
1994+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1995+
let chain_source = random_chain_source(&bitcoind, &electrsd);
1996+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
1997+
1998+
let addr_a = node_a.onchain_payment().new_address().unwrap();
1999+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2000+
2001+
let premine_amount_sat = 2_125_000;
2002+
premine_and_distribute_funds(
2003+
&bitcoind.client,
2004+
&electrsd.client,
2005+
vec![addr_a, addr_b],
2006+
Amount::from_sat(premine_amount_sat),
2007+
)
2008+
.await;
2009+
node_a.sync_wallets().unwrap();
2010+
node_b.sync_wallets().unwrap();
2011+
2012+
let funding_amount_sat = 2_080_000;
2013+
let push_msat = (funding_amount_sat / 2) * 1000;
2014+
open_channel_push_amt(&node_a, &node_b, funding_amount_sat, Some(push_msat), true, &electrsd)
2015+
.await;
2016+
2017+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2018+
node_a.sync_wallets().unwrap();
2019+
node_b.sync_wallets().unwrap();
2020+
2021+
expect_channel_ready_event!(node_a, node_b.node_id());
2022+
expect_channel_ready_event!(node_b, node_a.node_id());
2023+
2024+
// Sleep a bit for gossip to propagate.
2025+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
2026+
2027+
let invoice_description =
2028+
Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap());
2029+
let amount_msat = 2_500_000;
2030+
2031+
// Create an invoice on node_b and immediately cancel it.
2032+
let invoice =
2033+
node_b.bolt11_payment().receive(amount_msat, &invoice_description.clone(), 9217).unwrap();
2034+
2035+
let payment_hash = PaymentHash(invoice.payment_hash().0);
2036+
let payment_id = PaymentId(payment_hash.0);
2037+
node_b.bolt11_payment().cancel_invoice(payment_hash).unwrap();
2038+
2039+
// Verify the payment status is now Failed.
2040+
assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Failed);
2041+
2042+
// Attempting to pay the canceled invoice should result in a failure on the sender side.
2043+
node_a.bolt11_payment().send(&invoice, None).unwrap();
2044+
expect_event!(node_a, PaymentFailed);
2045+
2046+
// Verify cancelling an already claimed payment errors.
2047+
let invoice_2 =
2048+
node_b.bolt11_payment().receive(amount_msat, &invoice_description.clone(), 9217).unwrap();
2049+
let payment_id_2 = node_a.bolt11_payment().send(&invoice_2, None).unwrap();
2050+
expect_payment_received_event!(node_b, amount_msat);
2051+
expect_payment_successful_event!(node_a, Some(payment_id_2), None);
2052+
2053+
let payment_hash_2 = PaymentHash(invoice_2.payment_hash().0);
2054+
assert_eq!(
2055+
node_b.bolt11_payment().cancel_invoice(payment_hash_2),
2056+
Err(NodeError::InvalidPaymentHash)
2057+
);
2058+
2059+
node_a.stop().unwrap();
2060+
node_b.stop().unwrap();
2061+
}
2062+
19922063
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
19932064
async fn drop_in_async_context() {
19942065
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)