ldk_node/payment/
bolt12.rs

1// This file is Copyright its original authors, visible in version control history.
2//
3// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
6// accordance with one or both of these licenses.
7
8//! Holds a payment handler allowing to create and pay [BOLT 12] offers and refunds.
9//!
10//! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
11
12use std::num::NonZeroU64;
13use std::sync::{Arc, RwLock};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16use lightning::blinded_path::message::BlindedMessagePath;
17use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId, Retry};
18use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity};
19use lightning::offers::parse::Bolt12SemanticError;
20use lightning::routing::router::RouteParametersConfig;
21#[cfg(feature = "uniffi")]
22use lightning::util::ser::{Readable, Writeable};
23use lightning_types::string::UntrustedString;
24use rand::RngCore;
25
26use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT};
27use crate::error::Error;
28use crate::ffi::{maybe_deref, maybe_wrap};
29use crate::logger::{log_error, log_info, LdkLogger, Logger};
30use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
31use crate::types::{ChannelManager, PaymentStore};
32
33#[cfg(not(feature = "uniffi"))]
34type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice;
35#[cfg(feature = "uniffi")]
36type Bolt12Invoice = Arc<crate::ffi::Bolt12Invoice>;
37
38#[cfg(not(feature = "uniffi"))]
39type Offer = LdkOffer;
40#[cfg(feature = "uniffi")]
41type Offer = Arc<crate::ffi::Offer>;
42
43#[cfg(not(feature = "uniffi"))]
44type Refund = lightning::offers::refund::Refund;
45#[cfg(feature = "uniffi")]
46type Refund = Arc<crate::ffi::Refund>;
47
48/// A payment handler allowing to create and pay [BOLT 12] offers and refunds.
49///
50/// Should be retrieved by calling [`Node::bolt12_payment`].
51///
52/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
53/// [`Node::bolt12_payment`]: crate::Node::bolt12_payment
54pub struct Bolt12Payment {
55	channel_manager: Arc<ChannelManager>,
56	payment_store: Arc<PaymentStore>,
57	config: Arc<Config>,
58	is_running: Arc<RwLock<bool>>,
59	logger: Arc<Logger>,
60	async_payments_role: Option<AsyncPaymentsRole>,
61}
62
63impl Bolt12Payment {
64	pub(crate) fn new(
65		channel_manager: Arc<ChannelManager>, payment_store: Arc<PaymentStore>,
66		config: Arc<Config>, is_running: Arc<RwLock<bool>>, logger: Arc<Logger>,
67		async_payments_role: Option<AsyncPaymentsRole>,
68	) -> Self {
69		Self { channel_manager, payment_store, config, is_running, logger, async_payments_role }
70	}
71
72	/// Send a payment given an offer.
73	///
74	/// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice
75	/// response.
76	///
77	/// If `quantity` is `Some` it represents the number of items requested.
78	///
79	/// If `route_parameters` are provided they will override the default as well as the
80	/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
81	pub fn send(
82		&self, offer: &Offer, quantity: Option<u64>, payer_note: Option<String>,
83		route_parameters: Option<RouteParametersConfig>,
84	) -> Result<PaymentId, Error> {
85		if !*self.is_running.read().unwrap() {
86			return Err(Error::NotRunning);
87		}
88
89		let offer = maybe_deref(offer);
90
91		let mut random_bytes = [0u8; 32];
92		rand::rng().fill_bytes(&mut random_bytes);
93		let payment_id = PaymentId(random_bytes);
94		let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
95		let route_parameters =
96			route_parameters.or(self.config.route_parameters).unwrap_or_default();
97
98		let offer_amount_msat = match offer.amount() {
99			Some(Amount::Bitcoin { amount_msats }) => amount_msats,
100			Some(_) => {
101				log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency.");
102				return Err(Error::UnsupportedCurrency);
103			},
104			None => {
105				log_error!(self.logger, "Failed to send payment due to the given offer being \"zero-amount\". Please use send_using_amount instead.");
106				return Err(Error::InvalidOffer);
107			},
108		};
109
110		let params = OptionalOfferPaymentParams {
111			payer_note: payer_note.clone(),
112			retry_strategy,
113			route_params_config: route_parameters,
114		};
115		let res = if let Some(quantity) = quantity {
116			self.channel_manager
117				.pay_for_offer_with_quantity(&offer, None, payment_id, params, quantity)
118		} else {
119			self.channel_manager.pay_for_offer(&offer, None, payment_id, params)
120		};
121
122		match res {
123			Ok(()) => {
124				let payee_pubkey = offer.issuer_signing_pubkey();
125				log_info!(
126					self.logger,
127					"Initiated sending {}msat to {:?}",
128					offer_amount_msat,
129					payee_pubkey
130				);
131
132				let kind = PaymentKind::Bolt12Offer {
133					hash: None,
134					preimage: None,
135					secret: None,
136					offer_id: offer.id(),
137					payer_note: payer_note.map(UntrustedString),
138					quantity,
139				};
140				let payment = PaymentDetails::new(
141					payment_id,
142					kind,
143					Some(offer_amount_msat),
144					None,
145					PaymentDirection::Outbound,
146					PaymentStatus::Pending,
147				);
148				self.payment_store.insert(payment)?;
149
150				Ok(payment_id)
151			},
152			Err(e) => {
153				log_error!(self.logger, "Failed to send invoice request: {:?}", e);
154				match e {
155					Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment),
156					_ => {
157						let kind = PaymentKind::Bolt12Offer {
158							hash: None,
159							preimage: None,
160							secret: None,
161							offer_id: offer.id(),
162							payer_note: payer_note.map(UntrustedString),
163							quantity,
164						};
165						let payment = PaymentDetails::new(
166							payment_id,
167							kind,
168							Some(offer_amount_msat),
169							None,
170							PaymentDirection::Outbound,
171							PaymentStatus::Failed,
172						);
173						self.payment_store.insert(payment)?;
174						Err(Error::InvoiceRequestCreationFailed)
175					},
176				}
177			},
178		}
179	}
180
181	/// Send a payment given an offer and an amount in millisatoshi.
182	///
183	/// This will fail if the amount given is less than the value required by the given offer.
184	///
185	/// This can be used to pay a so-called "zero-amount" offers, i.e., an offer that leaves the
186	/// amount paid to be determined by the user.
187	///
188	/// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice
189	/// response.
190	///
191	/// If `route_parameters` are provided they will override the default as well as the
192	/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
193	pub fn send_using_amount(
194		&self, offer: &Offer, amount_msat: u64, quantity: Option<u64>, payer_note: Option<String>,
195		route_parameters: Option<RouteParametersConfig>,
196	) -> Result<PaymentId, Error> {
197		if !*self.is_running.read().unwrap() {
198			return Err(Error::NotRunning);
199		}
200
201		let offer = maybe_deref(offer);
202
203		let mut random_bytes = [0u8; 32];
204		rand::rng().fill_bytes(&mut random_bytes);
205		let payment_id = PaymentId(random_bytes);
206		let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
207		let route_parameters =
208			route_parameters.or(self.config.route_parameters).unwrap_or_default();
209
210		let offer_amount_msat = match offer.amount() {
211			Some(Amount::Bitcoin { amount_msats }) => amount_msats,
212			Some(_) => {
213				log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency.");
214				return Err(Error::UnsupportedCurrency);
215			},
216			None => amount_msat,
217		};
218
219		if amount_msat < offer_amount_msat {
220			log_error!(
221				self.logger,
222				"Failed to pay as the given amount needs to be at least the offer amount: required {}msat, gave {}msat.", offer_amount_msat, amount_msat);
223			return Err(Error::InvalidAmount);
224		}
225
226		let params = OptionalOfferPaymentParams {
227			payer_note: payer_note.clone(),
228			retry_strategy,
229			route_params_config: route_parameters,
230		};
231		let res = if let Some(quantity) = quantity {
232			self.channel_manager.pay_for_offer_with_quantity(
233				&offer,
234				Some(amount_msat),
235				payment_id,
236				params,
237				quantity,
238			)
239		} else {
240			self.channel_manager.pay_for_offer(&offer, Some(amount_msat), payment_id, params)
241		};
242
243		match res {
244			Ok(()) => {
245				let payee_pubkey = offer.issuer_signing_pubkey();
246				log_info!(
247					self.logger,
248					"Initiated sending {}msat to {:?}",
249					amount_msat,
250					payee_pubkey
251				);
252
253				let kind = PaymentKind::Bolt12Offer {
254					hash: None,
255					preimage: None,
256					secret: None,
257					offer_id: offer.id(),
258					payer_note: payer_note.map(UntrustedString),
259					quantity,
260				};
261				let payment = PaymentDetails::new(
262					payment_id,
263					kind,
264					Some(amount_msat),
265					None,
266					PaymentDirection::Outbound,
267					PaymentStatus::Pending,
268				);
269				self.payment_store.insert(payment)?;
270
271				Ok(payment_id)
272			},
273			Err(e) => {
274				log_error!(self.logger, "Failed to send payment: {:?}", e);
275				match e {
276					Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment),
277					_ => {
278						let kind = PaymentKind::Bolt12Offer {
279							hash: None,
280							preimage: None,
281							secret: None,
282							offer_id: offer.id(),
283							payer_note: payer_note.map(UntrustedString),
284							quantity,
285						};
286						let payment = PaymentDetails::new(
287							payment_id,
288							kind,
289							Some(amount_msat),
290							None,
291							PaymentDirection::Outbound,
292							PaymentStatus::Failed,
293						);
294						self.payment_store.insert(payment)?;
295						Err(Error::PaymentSendingFailed)
296					},
297				}
298			},
299		}
300	}
301
302	pub(crate) fn receive_inner(
303		&self, amount_msat: u64, description: &str, expiry_secs: Option<u32>, quantity: Option<u64>,
304	) -> Result<LdkOffer, Error> {
305		let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| {
306			log_error!(self.logger, "Failed to create offer builder: {:?}", e);
307			Error::OfferCreationFailed
308		})?;
309
310		if let Some(expiry_secs) = expiry_secs {
311			let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
312				.duration_since(UNIX_EPOCH)
313				.unwrap();
314			offer_builder = offer_builder.absolute_expiry(absolute_expiry);
315		}
316
317		let mut offer =
318			offer_builder.amount_msats(amount_msat).description(description.to_string());
319
320		if let Some(qty) = quantity {
321			if qty == 0 {
322				log_error!(self.logger, "Failed to create offer: quantity can't be zero.");
323				return Err(Error::InvalidQuantity);
324			} else {
325				offer = offer.supported_quantity(Quantity::Bounded(NonZeroU64::new(qty).unwrap()))
326			};
327		};
328
329		let finalized_offer = offer.build().map_err(|e| {
330			log_error!(self.logger, "Failed to create offer: {:?}", e);
331			Error::OfferCreationFailed
332		})?;
333
334		Ok(finalized_offer)
335	}
336
337	/// Returns a payable offer that can be used to request and receive a payment of the amount
338	/// given.
339	pub fn receive(
340		&self, amount_msat: u64, description: &str, expiry_secs: Option<u32>, quantity: Option<u64>,
341	) -> Result<Offer, Error> {
342		let offer = self.receive_inner(amount_msat, description, expiry_secs, quantity)?;
343		Ok(maybe_wrap(offer))
344	}
345
346	/// Returns a payable offer that can be used to request and receive a payment for which the
347	/// amount is to be determined by the user, also known as a "zero-amount" offer.
348	pub fn receive_variable_amount(
349		&self, description: &str, expiry_secs: Option<u32>,
350	) -> Result<Offer, Error> {
351		let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| {
352			log_error!(self.logger, "Failed to create offer builder: {:?}", e);
353			Error::OfferCreationFailed
354		})?;
355
356		if let Some(expiry_secs) = expiry_secs {
357			let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
358				.duration_since(UNIX_EPOCH)
359				.unwrap();
360			offer_builder = offer_builder.absolute_expiry(absolute_expiry);
361		}
362
363		let offer = offer_builder.description(description.to_string()).build().map_err(|e| {
364			log_error!(self.logger, "Failed to create offer: {:?}", e);
365			Error::OfferCreationFailed
366		})?;
367
368		Ok(maybe_wrap(offer))
369	}
370
371	/// Requests a refund payment for the given [`Refund`].
372	///
373	/// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to
374	/// retrieve the refund).
375	///
376	/// [`Refund`]: lightning::offers::refund::Refund
377	/// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice
378	pub fn request_refund_payment(&self, refund: &Refund) -> Result<Bolt12Invoice, Error> {
379		if !*self.is_running.read().unwrap() {
380			return Err(Error::NotRunning);
381		}
382
383		let refund = maybe_deref(refund);
384		let invoice = self.channel_manager.request_refund_payment(&refund).map_err(|e| {
385			log_error!(self.logger, "Failed to request refund payment: {:?}", e);
386			Error::InvoiceRequestCreationFailed
387		})?;
388
389		let payment_hash = invoice.payment_hash();
390		let payment_id = PaymentId(payment_hash.0);
391
392		let kind = PaymentKind::Bolt12Refund {
393			hash: Some(payment_hash),
394			preimage: None,
395			secret: None,
396			payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())),
397			quantity: refund.quantity(),
398		};
399
400		let payment = PaymentDetails::new(
401			payment_id,
402			kind,
403			Some(refund.amount_msats()),
404			None,
405			PaymentDirection::Inbound,
406			PaymentStatus::Pending,
407		);
408
409		self.payment_store.insert(payment)?;
410
411		Ok(maybe_wrap(invoice))
412	}
413
414	/// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given.
415	///
416	/// If `route_parameters` are provided they will override the default as well as the
417	/// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis.
418	///
419	/// [`Refund`]: lightning::offers::refund::Refund
420	pub fn initiate_refund(
421		&self, amount_msat: u64, expiry_secs: u32, quantity: Option<u64>,
422		payer_note: Option<String>, route_parameters: Option<RouteParametersConfig>,
423	) -> Result<Refund, Error> {
424		let mut random_bytes = [0u8; 32];
425		rand::rng().fill_bytes(&mut random_bytes);
426		let payment_id = PaymentId(random_bytes);
427
428		let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
429			.duration_since(UNIX_EPOCH)
430			.unwrap();
431		let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
432		let route_parameters =
433			route_parameters.or(self.config.route_parameters).unwrap_or_default();
434
435		let mut refund_builder = self
436			.channel_manager
437			.create_refund_builder(
438				amount_msat,
439				absolute_expiry,
440				payment_id,
441				retry_strategy,
442				route_parameters,
443			)
444			.map_err(|e| {
445				log_error!(self.logger, "Failed to create refund builder: {:?}", e);
446				Error::RefundCreationFailed
447			})?;
448
449		if let Some(qty) = quantity {
450			refund_builder = refund_builder.quantity(qty);
451		}
452
453		if let Some(note) = payer_note.clone() {
454			refund_builder = refund_builder.payer_note(note);
455		}
456
457		let refund = refund_builder.build().map_err(|e| {
458			log_error!(self.logger, "Failed to create refund: {:?}", e);
459			Error::RefundCreationFailed
460		})?;
461
462		log_info!(self.logger, "Offering refund of {}msat", amount_msat);
463
464		let kind = PaymentKind::Bolt12Refund {
465			hash: None,
466			preimage: None,
467			secret: None,
468			payer_note: payer_note.map(|note| UntrustedString(note)),
469			quantity,
470		};
471		let payment = PaymentDetails::new(
472			payment_id,
473			kind,
474			Some(amount_msat),
475			None,
476			PaymentDirection::Outbound,
477			PaymentStatus::Pending,
478		);
479
480		self.payment_store.insert(payment)?;
481
482		Ok(maybe_wrap(refund))
483	}
484
485	/// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient.
486	///
487	/// Will only return an offer if [`Bolt12Payment::set_paths_to_static_invoice_server`] was called and we succeeded
488	/// in interactively building a [`StaticInvoice`] with the static invoice server.
489	///
490	/// Useful for posting offers to receive payments later, such as posting an offer on a website.
491	///
492	/// **Caution**: Async payments support is considered experimental.
493	///
494	/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
495	/// [`Offer`]: lightning::offers::offer::Offer
496	pub fn receive_async(&self) -> Result<Offer, Error> {
497		self.channel_manager
498			.get_async_receive_offer()
499			.map(maybe_wrap)
500			.or(Err(Error::OfferCreationFailed))
501	}
502
503	/// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a
504	/// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline.
505	///
506	/// **Caution**: Async payments support is considered experimental.
507	///
508	/// [`Offer`]: lightning::offers::offer::Offer
509	/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
510	#[cfg(not(feature = "uniffi"))]
511	pub fn set_paths_to_static_invoice_server(
512		&self, paths: Vec<BlindedMessagePath>,
513	) -> Result<(), Error> {
514		self.channel_manager
515			.set_paths_to_static_invoice_server(paths)
516			.or(Err(Error::InvalidBlindedPaths))
517	}
518
519	/// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a
520	/// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline.
521	///
522	/// **Caution**: Async payments support is considered experimental.
523	///
524	/// [`Offer`]: lightning::offers::offer::Offer
525	/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
526	#[cfg(feature = "uniffi")]
527	pub fn set_paths_to_static_invoice_server(&self, paths: Vec<u8>) -> Result<(), Error> {
528		let decoded_paths = <Vec<BlindedMessagePath> as Readable>::read(&mut &paths[..])
529			.or(Err(Error::InvalidBlindedPaths))?;
530
531		self.channel_manager
532			.set_paths_to_static_invoice_server(decoded_paths)
533			.or(Err(Error::InvalidBlindedPaths))
534	}
535
536	/// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively
537	/// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments.
538	///
539	/// **Caution**: Async payments support is considered experimental.
540	///
541	/// [`Offer`]: lightning::offers::offer::Offer
542	/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
543	#[cfg(not(feature = "uniffi"))]
544	pub fn blinded_paths_for_async_recipient(
545		&self, recipient_id: Vec<u8>,
546	) -> Result<Vec<BlindedMessagePath>, Error> {
547		self.blinded_paths_for_async_recipient_internal(recipient_id)
548	}
549
550	/// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively
551	/// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments.
552	///
553	/// **Caution**: Async payments support is considered experimental.
554	///
555	/// [`Offer`]: lightning::offers::offer::Offer
556	/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice
557	#[cfg(feature = "uniffi")]
558	pub fn blinded_paths_for_async_recipient(
559		&self, recipient_id: Vec<u8>,
560	) -> Result<Vec<u8>, Error> {
561		let paths = self.blinded_paths_for_async_recipient_internal(recipient_id)?;
562
563		let mut bytes = Vec::new();
564		paths.write(&mut bytes).or(Err(Error::InvalidBlindedPaths))?;
565		Ok(bytes)
566	}
567
568	fn blinded_paths_for_async_recipient_internal(
569		&self, recipient_id: Vec<u8>,
570	) -> Result<Vec<BlindedMessagePath>, Error> {
571		match self.async_payments_role {
572			Some(AsyncPaymentsRole::Server) => {},
573			_ => {
574				return Err(Error::AsyncPaymentServicesDisabled);
575			},
576		}
577
578		self.channel_manager
579			.blinded_paths_for_async_recipient(recipient_id, None)
580			.or(Err(Error::InvalidBlindedPaths))
581	}
582}