Skip to content

Commit 74f3c9a

Browse files
committed
feat: manually handle BOLT12 invoices
1 parent 804f00f commit 74f3c9a

File tree

7 files changed

+321
-4
lines changed

7 files changed

+321
-4
lines changed

src/builder.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,8 @@ fn build_with_store_internal(
18831883

18841884
let pathfinding_scores_sync_url = pathfinding_scores_sync_config.map(|c| c.url.clone());
18851885

1886+
let pending_bolt12_invoice_contexts = Arc::new(Mutex::new(HashMap::new()));
1887+
18861888
#[cfg(cycle_tests)]
18871889
let mut _leak_checker = crate::LeakChecker(Vec::new());
18881890
#[cfg(cycle_tests)]
@@ -1930,6 +1932,7 @@ fn build_with_store_internal(
19301932
hrn_resolver,
19311933
#[cfg(cycle_tests)]
19321934
_leak_checker,
1935+
pending_bolt12_invoice_contexts,
19331936
})
19341937
}
19351938

src/config.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,20 @@ pub struct Config {
204204
///
205205
/// **Note**: If unset, connecting to peer OnionV3 addresses will fail.
206206
pub tor_config: Option<TorConfig>,
207+
/// If set to `true`, BOLT12 invoices will not be paid automatically when received. Instead, an
208+
/// [`Event::Bolt12InvoiceReceived`] event will be emitted, allowing inspection of the invoice
209+
/// before explicitly paying via [`Bolt12Payment::send_payment_for_bolt12_invoice`] or
210+
/// abandoning via [`Bolt12Payment::abandon_bolt12_invoice`].
211+
///
212+
/// **Note:** If the invoice is not paid or abandoned before the next LDK timer tick, the
213+
/// payment will be timed out automatically.
214+
///
215+
/// Default value: `false`
216+
///
217+
/// [`Event::Bolt12InvoiceReceived`]: crate::Event::Bolt12InvoiceReceived
218+
/// [`Bolt12Payment::send_payment_for_bolt12_invoice`]: crate::payment::Bolt12Payment::send_payment_for_bolt12_invoice
219+
/// [`Bolt12Payment::abandon_bolt12_invoice`]: crate::payment::Bolt12Payment::abandon_bolt12_invoice
220+
pub manually_handle_bolt12_invoices: bool,
207221
}
208222

209223
impl Default for Config {
@@ -219,6 +233,7 @@ impl Default for Config {
219233
tor_config: None,
220234
route_parameters: None,
221235
node_alias: None,
236+
manually_handle_bolt12_invoices: false,
222237
}
223238
}
224239
}
@@ -347,6 +362,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig {
347362
user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx =
348363
config.anchor_channels_config.is_some();
349364
user_config.reject_inbound_splices = false;
365+
user_config.manually_handle_bolt12_invoices = config.manually_handle_bolt12_invoices;
350366

351367
if may_announce_channel(config).is_err() {
352368
user_config.accept_forwards_to_priv_channels = false;

src/event.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use crate::io::{
4747
};
4848
use crate::liquidity::LiquiditySource;
4949
use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
50+
use crate::payment::PendingBolt12InvoiceContexts;
5051
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
5152
use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore;
5253
use crate::payment::store::{
@@ -275,6 +276,25 @@ pub enum Event {
275276
/// The outpoint of the channel's splice funding transaction, if one was created.
276277
abandoned_funding_txo: Option<OutPoint>,
277278
},
279+
/// A BOLT12 invoice has been received, and is waiting to be paid or abandoned.
280+
///
281+
/// This event will only be generated if [`Config::manually_handle_bolt12_invoices`] is set
282+
/// to `true`.
283+
///
284+
/// Call [`Bolt12Payment::send_payment_for_bolt12_invoice`] to pay the invoice or
285+
/// [`Bolt12Payment::abandon_bolt12_invoice`] to abandon it.
286+
///
287+
/// [`Config::manually_handle_bolt12_invoices`]: crate::config::Config::manually_handle_bolt12_invoices
288+
/// [`Bolt12Payment::send_payment_for_bolt12_invoice`]: crate::payment::Bolt12Payment::send_payment_for_bolt12_invoice
289+
/// [`Bolt12Payment::abandon_bolt12_invoice`]: crate::payment::Bolt12Payment::abandon_bolt12_invoice
290+
Bolt12InvoiceReceived {
291+
/// A local identifier used to track the payment.
292+
payment_id: PaymentId,
293+
/// The hash of the payment as specified in the invoice.
294+
payment_hash: PaymentHash,
295+
/// The amount in millisatoshis specified in the invoice.
296+
amount_msat: u64,
297+
},
278298
}
279299

280300
impl_writeable_tlv_based_enum!(Event,
@@ -346,6 +366,11 @@ impl_writeable_tlv_based_enum!(Event,
346366
(5, user_channel_id, required),
347367
(7, abandoned_funding_txo, option),
348368
},
369+
(10, Bolt12InvoiceReceived) => {
370+
(0, payment_id, required),
371+
(2, payment_hash, required),
372+
(4, amount_msat, required),
373+
},
349374
);
350375

351376
pub struct EventQueue<L: Deref>
@@ -515,6 +540,7 @@ where
515540
static_invoice_store: Option<StaticInvoiceStore>,
516541
onion_messenger: Arc<OnionMessenger>,
517542
om_mailbox: Option<Arc<OnionMessageMailbox>>,
543+
pending_bolt12_invoice_contexts: PendingBolt12InvoiceContexts,
518544
}
519545

520546
impl<L: Deref + Clone + Sync + Send + 'static> EventHandler<L>
@@ -531,6 +557,7 @@ where
531557
keys_manager: Arc<KeysManager>, static_invoice_store: Option<StaticInvoiceStore>,
532558
onion_messenger: Arc<OnionMessenger>, om_mailbox: Option<Arc<OnionMessageMailbox>>,
533559
runtime: Arc<Runtime>, logger: L, config: Arc<Config>,
560+
pending_bolt12_invoice_contexts: PendingBolt12InvoiceContexts,
534561
) -> Self {
535562
Self {
536563
event_queue,
@@ -550,6 +577,7 @@ where
550577
static_invoice_store,
551578
onion_messenger,
552579
om_mailbox,
580+
pending_bolt12_invoice_contexts,
553581
}
554582
}
555583

@@ -1568,8 +1596,35 @@ where
15681596
.await;
15691597
}
15701598
},
1571-
LdkEvent::InvoiceReceived { .. } => {
1572-
debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted.");
1599+
LdkEvent::InvoiceReceived { payment_id, invoice, context, .. } => {
1600+
let amount_msat = invoice.amount_msats();
1601+
let payment_hash = invoice.payment_hash();
1602+
log_info!(
1603+
self.logger,
1604+
"Received BOLT12 invoice for payment_id {} with amount {}msat for manual handling",
1605+
payment_id,
1606+
amount_msat,
1607+
);
1608+
1609+
self.pending_bolt12_invoice_contexts
1610+
.lock()
1611+
.unwrap()
1612+
.insert(payment_id, (invoice, context));
1613+
1614+
self.event_queue
1615+
.add_event(Event::Bolt12InvoiceReceived {
1616+
payment_id,
1617+
payment_hash,
1618+
amount_msat,
1619+
})
1620+
.await
1621+
.unwrap_or_else(|e| {
1622+
log_error!(
1623+
self.logger,
1624+
"Failed to push Bolt12InvoiceReceived event: {}",
1625+
e
1626+
);
1627+
});
15731628
},
15741629
LdkEvent::ConnectionNeeded { node_id, addresses } => {
15751630
let spawn_logger = self.logger.clone();

src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ use types::{
180180
pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, SyncAndAsyncKVStore, UserChannelId};
181181
pub use vss_client;
182182

183+
use crate::payment::PendingBolt12InvoiceContexts;
183184
use crate::scoring::setup_background_pathfinding_scores_sync;
184185
use crate::wallet::FundingAmount;
185186

@@ -239,6 +240,7 @@ pub struct Node {
239240
om_mailbox: Option<Arc<OnionMessageMailbox>>,
240241
async_payments_role: Option<AsyncPaymentsRole>,
241242
hrn_resolver: Arc<HRNResolver>,
243+
pending_bolt12_invoice_contexts: PendingBolt12InvoiceContexts,
242244
#[cfg(cycle_tests)]
243245
_leak_checker: LeakChecker,
244246
}
@@ -593,6 +595,7 @@ impl Node {
593595
Arc::clone(&self.runtime),
594596
Arc::clone(&self.logger),
595597
Arc::clone(&self.config),
598+
Arc::clone(&self.pending_bolt12_invoice_contexts),
596599
));
597600

598601
// Setup background processing
@@ -908,6 +911,7 @@ impl Node {
908911
Arc::clone(&self.is_running),
909912
Arc::clone(&self.logger),
910913
self.async_payments_role,
914+
Arc::clone(&self.pending_bolt12_invoice_contexts),
911915
)
912916
}
913917

@@ -924,6 +928,7 @@ impl Node {
924928
Arc::clone(&self.is_running),
925929
Arc::clone(&self.logger),
926930
self.async_payments_role,
931+
Arc::clone(&self.pending_bolt12_invoice_contexts),
927932
))
928933
}
929934

src/payment/bolt12.rs

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
//!
1010
//! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
1111
12+
use std::collections::HashMap;
1213
use std::num::NonZeroU64;
13-
use std::sync::{Arc, RwLock};
14+
use std::sync::{Arc, Mutex, RwLock};
1415
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1516

16-
use lightning::blinded_path::message::BlindedMessagePath;
17+
use lightning::blinded_path::message::{BlindedMessagePath, OffersContext};
1718
use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId};
1819
use lightning::ln::outbound_payment::Retry;
1920
use lightning::offers::offer::{Amount, Offer as LdkOffer, OfferFromHrn, Quantity};
@@ -51,6 +52,14 @@ type HumanReadableName = lightning::onion_message::dns_resolution::HumanReadable
5152
#[cfg(feature = "uniffi")]
5253
type HumanReadableName = Arc<crate::ffi::HumanReadableName>;
5354

55+
/// Holds a pending BOLT12 invoice and its associated context, for use with manual invoice
56+
/// handling. See [`Config::manually_handle_bolt12_invoices`].
57+
///
58+
/// [`Config::manually_handle_bolt12_invoices`]: crate::config::Config::manually_handle_bolt12_invoices
59+
pub(crate) type PendingBolt12InvoiceContexts = Arc<
60+
Mutex<HashMap<PaymentId, (lightning::offers::invoice::Bolt12Invoice, Option<OffersContext>)>>,
61+
>;
62+
5463
/// A payment handler allowing to create and pay [BOLT 12] offers and refunds.
5564
///
5665
/// Should be retrieved by calling [`Node::bolt12_payment`].
@@ -66,13 +75,15 @@ pub struct Bolt12Payment {
6675
is_running: Arc<RwLock<bool>>,
6776
logger: Arc<Logger>,
6877
async_payments_role: Option<AsyncPaymentsRole>,
78+
pending_bolt12_invoice_contexts: PendingBolt12InvoiceContexts,
6979
}
7080

7181
impl Bolt12Payment {
7282
pub(crate) fn new(
7383
channel_manager: Arc<ChannelManager>, keys_manager: Arc<KeysManager>,
7484
payment_store: Arc<PaymentStore>, config: Arc<Config>, is_running: Arc<RwLock<bool>>,
7585
logger: Arc<Logger>, async_payments_role: Option<AsyncPaymentsRole>,
86+
pending_bolt12_invoice_contexts: PendingBolt12InvoiceContexts,
7687
) -> Self {
7788
Self {
7889
channel_manager,
@@ -82,6 +93,7 @@ impl Bolt12Payment {
8293
is_running,
8394
logger,
8495
async_payments_role,
96+
pending_bolt12_invoice_contexts,
8597
}
8698
}
8799

@@ -577,6 +589,70 @@ impl Bolt12Payment {
577589
) -> Result<Vec<BlindedMessagePath>, Error> {
578590
self.blinded_paths_for_async_recipient_internal(recipient_id)
579591
}
592+
593+
/// Pays a BOLT12 invoice that was previously received via an
594+
/// [`Event::Bolt12InvoiceReceived`] event.
595+
///
596+
/// This is only relevant when [`Config::manually_handle_bolt12_invoices`] is set to `true`.
597+
///
598+
/// Returns an [`Error::InvalidPaymentId`] if no pending invoice is found for the given
599+
/// `payment_id`.
600+
///
601+
/// [`Event::Bolt12InvoiceReceived`]: crate::Event::Bolt12InvoiceReceived
602+
/// [`Config::manually_handle_bolt12_invoices`]: crate::config::Config::manually_handle_bolt12_invoices
603+
pub fn send_payment_for_bolt12_invoice(&self, payment_id: PaymentId) -> Result<(), Error> {
604+
let (invoice, context) = self
605+
.pending_bolt12_invoice_contexts
606+
.lock()
607+
.unwrap()
608+
.remove(&payment_id)
609+
.ok_or(Error::InvalidPaymentId)?;
610+
611+
self.channel_manager.send_payment_for_bolt12_invoice(&invoice, context.as_ref()).map_err(
612+
|e| {
613+
log_error!(self.logger, "Failed to send payment for BOLT12 invoice: {:?}", e);
614+
Error::PaymentSendingFailed
615+
},
616+
)?;
617+
618+
log_info!(
619+
self.logger,
620+
"Initiated payment for manually-handled BOLT12 invoice with payment_id {}",
621+
payment_id
622+
);
623+
Ok(())
624+
}
625+
626+
/// Abandons a BOLT12 invoice that was previously received via an
627+
/// [`Event::Bolt12InvoiceReceived`] event.
628+
///
629+
/// This is only relevant when [`Config::manually_handle_bolt12_invoices`] is set to `true`.
630+
/// Use this to reject an invoice you don't want to pay. This will result in an
631+
/// [`Event::PaymentFailed`] being emitted.
632+
///
633+
/// Returns an [`Error::InvalidPaymentId`] if no pending invoice is found for the given
634+
/// `payment_id`.
635+
///
636+
/// [`Event::Bolt12InvoiceReceived`]: crate::Event::Bolt12InvoiceReceived
637+
/// [`Event::PaymentFailed`]: crate::Event::PaymentFailed
638+
/// [`Config::manually_handle_bolt12_invoices`]: crate::config::Config::manually_handle_bolt12_invoices
639+
pub fn abandon_bolt12_invoice(&self, payment_id: PaymentId) -> Result<(), Error> {
640+
let _removed = self
641+
.pending_bolt12_invoice_contexts
642+
.lock()
643+
.unwrap()
644+
.remove(&payment_id)
645+
.ok_or(Error::InvalidPaymentId)?;
646+
647+
self.channel_manager.abandon_payment(payment_id);
648+
649+
log_info!(
650+
self.logger,
651+
"Abandoned manually-handled BOLT12 invoice with payment_id {}",
652+
payment_id
653+
);
654+
Ok(())
655+
}
580656
}
581657

582658
#[cfg(feature = "uniffi")]
@@ -614,4 +690,68 @@ impl Bolt12Payment {
614690
paths.write(&mut bytes).or(Err(Error::InvalidBlindedPaths))?;
615691
Ok(bytes)
616692
}
693+
694+
/// Pays a BOLT12 invoice that was previously received via an
695+
/// [`Event::Bolt12InvoiceReceived`] event.
696+
///
697+
/// This is only relevant when [`Config::manually_handle_bolt12_invoices`] is set to `true`.
698+
///
699+
/// Returns an [`Error::InvalidPaymentId`] if no pending invoice is found for the given
700+
/// `payment_id`.
701+
///
702+
/// [`Event::Bolt12InvoiceReceived`]: crate::Event::Bolt12InvoiceReceived
703+
/// [`Config::manually_handle_bolt12_invoices`]: crate::config::Config::manually_handle_bolt12_invoices
704+
pub fn send_payment_for_bolt12_invoice(&self, payment_id: PaymentId) -> Result<(), Error> {
705+
let (invoice, context) = self
706+
.pending_bolt12_invoice_contexts
707+
.lock()
708+
.unwrap()
709+
.remove(&payment_id)
710+
.ok_or(Error::InvalidPaymentId)?;
711+
712+
self.channel_manager.send_payment_for_bolt12_invoice(&invoice, context.as_ref()).map_err(
713+
|e| {
714+
log_error!(self.logger, "Failed to send payment for BOLT12 invoice: {:?}", e);
715+
Error::PaymentSendingFailed
716+
},
717+
)?;
718+
719+
log_info!(
720+
self.logger,
721+
"Initiated payment for manually-handled BOLT12 invoice with payment_id {}",
722+
payment_id
723+
);
724+
Ok(())
725+
}
726+
727+
/// Abandons a BOLT12 invoice that was previously received via an
728+
/// [`Event::Bolt12InvoiceReceived`] event.
729+
///
730+
/// This is only relevant when [`Config::manually_handle_bolt12_invoices`] is set to `true`.
731+
/// Use this to reject an invoice you don't want to pay. This will result in an
732+
/// [`Event::PaymentFailed`] being emitted.
733+
///
734+
/// Returns an [`Error::InvalidPaymentId`] if no pending invoice is found for the given
735+
/// `payment_id`.
736+
///
737+
/// [`Event::Bolt12InvoiceReceived`]: crate::Event::Bolt12InvoiceReceived
738+
/// [`Event::PaymentFailed`]: crate::Event::PaymentFailed
739+
/// [`Config::manually_handle_bolt12_invoices`]: crate::config::Config::manually_handle_bolt12_invoices
740+
pub fn abandon_bolt12_invoice(&self, payment_id: PaymentId) -> Result<(), Error> {
741+
let _removed = self
742+
.pending_bolt12_invoice_contexts
743+
.lock()
744+
.unwrap()
745+
.remove(&payment_id)
746+
.ok_or(Error::InvalidPaymentId)?;
747+
748+
self.channel_manager.abandon_payment(payment_id);
749+
750+
log_info!(
751+
self.logger,
752+
"Abandoned manually-handled BOLT12 invoice with payment_id {}",
753+
payment_id
754+
);
755+
Ok(())
756+
}
617757
}

0 commit comments

Comments
 (0)