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 crate::config::LDK_PAYMENT_RETRY_TIMEOUT;
13use crate::error::Error;
14use crate::logger::{log_error, log_info, LdkLogger, Logger};
15use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
16use crate::types::{ChannelManager, PaymentStore};
17
18use lightning::ln::channelmanager::{PaymentId, Retry};
19use lightning::offers::invoice::Bolt12Invoice;
20use lightning::offers::offer::{Amount, Offer, Quantity};
21use lightning::offers::parse::Bolt12SemanticError;
22use lightning::offers::refund::Refund;
23use lightning::util::string::UntrustedString;
24
25use rand::RngCore;
26
27use std::num::NonZeroU64;
28use std::sync::{Arc, RwLock};
29use std::time::{Duration, SystemTime, UNIX_EPOCH};
30
31/// A payment handler allowing to create and pay [BOLT 12] offers and refunds.
32///
33/// Should be retrieved by calling [`Node::bolt12_payment`].
34///
35/// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md
36/// [`Node::bolt12_payment`]: crate::Node::bolt12_payment
37pub struct Bolt12Payment {
38	runtime: Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>>,
39	channel_manager: Arc<ChannelManager>,
40	payment_store: Arc<PaymentStore>,
41	logger: Arc<Logger>,
42}
43
44impl Bolt12Payment {
45	pub(crate) fn new(
46		runtime: Arc<RwLock<Option<Arc<tokio::runtime::Runtime>>>>,
47		channel_manager: Arc<ChannelManager>, payment_store: Arc<PaymentStore>,
48		logger: Arc<Logger>,
49	) -> Self {
50		Self { runtime, channel_manager, payment_store, logger }
51	}
52
53	/// Send a payment given an offer.
54	///
55	/// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice
56	/// response.
57	///
58	/// If `quantity` is `Some` it represents the number of items requested.
59	pub fn send(
60		&self, offer: &Offer, quantity: Option<u64>, payer_note: Option<String>,
61	) -> Result<PaymentId, Error> {
62		let rt_lock = self.runtime.read().unwrap();
63		if rt_lock.is_none() {
64			return Err(Error::NotRunning);
65		}
66		let mut random_bytes = [0u8; 32];
67		rand::thread_rng().fill_bytes(&mut random_bytes);
68		let payment_id = PaymentId(random_bytes);
69		let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
70		let max_total_routing_fee_msat = None;
71
72		let offer_amount_msat = match offer.amount() {
73			Some(Amount::Bitcoin { amount_msats }) => amount_msats,
74			Some(_) => {
75				log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency.");
76				return Err(Error::UnsupportedCurrency);
77			},
78			None => {
79				log_error!(self.logger, "Failed to send payment due to the given offer being \"zero-amount\". Please use send_using_amount instead.");
80				return Err(Error::InvalidOffer);
81			},
82		};
83
84		match self.channel_manager.pay_for_offer(
85			&offer,
86			quantity,
87			None,
88			payer_note.clone(),
89			payment_id,
90			retry_strategy,
91			max_total_routing_fee_msat,
92		) {
93			Ok(()) => {
94				let payee_pubkey = offer.issuer_signing_pubkey();
95				log_info!(
96					self.logger,
97					"Initiated sending {}msat to {:?}",
98					offer_amount_msat,
99					payee_pubkey
100				);
101
102				let kind = PaymentKind::Bolt12Offer {
103					hash: None,
104					preimage: None,
105					secret: None,
106					offer_id: offer.id(),
107					payer_note: payer_note.map(UntrustedString),
108					quantity,
109				};
110				let payment = PaymentDetails::new(
111					payment_id,
112					kind,
113					Some(offer_amount_msat),
114					None,
115					PaymentDirection::Outbound,
116					PaymentStatus::Pending,
117				);
118				self.payment_store.insert(payment)?;
119
120				Ok(payment_id)
121			},
122			Err(e) => {
123				log_error!(self.logger, "Failed to send invoice request: {:?}", e);
124				match e {
125					Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment),
126					_ => {
127						let kind = PaymentKind::Bolt12Offer {
128							hash: None,
129							preimage: None,
130							secret: None,
131							offer_id: offer.id(),
132							payer_note: payer_note.map(UntrustedString),
133							quantity,
134						};
135						let payment = PaymentDetails::new(
136							payment_id,
137							kind,
138							Some(offer_amount_msat),
139							None,
140							PaymentDirection::Outbound,
141							PaymentStatus::Failed,
142						);
143						self.payment_store.insert(payment)?;
144						Err(Error::InvoiceRequestCreationFailed)
145					},
146				}
147			},
148		}
149	}
150
151	/// Send a payment given an offer and an amount in millisatoshi.
152	///
153	/// This will fail if the amount given is less than the value required by the given offer.
154	///
155	/// This can be used to pay a so-called "zero-amount" offers, i.e., an offer that leaves the
156	/// amount paid to be determined by the user.
157	///
158	/// If `payer_note` is `Some` it will be seen by the recipient and reflected back in the invoice
159	/// response.
160	pub fn send_using_amount(
161		&self, offer: &Offer, amount_msat: u64, quantity: Option<u64>, payer_note: Option<String>,
162	) -> Result<PaymentId, Error> {
163		let rt_lock = self.runtime.read().unwrap();
164		if rt_lock.is_none() {
165			return Err(Error::NotRunning);
166		}
167
168		let mut random_bytes = [0u8; 32];
169		rand::thread_rng().fill_bytes(&mut random_bytes);
170		let payment_id = PaymentId(random_bytes);
171		let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
172		let max_total_routing_fee_msat = None;
173
174		let offer_amount_msat = match offer.amount() {
175			Some(Amount::Bitcoin { amount_msats }) => amount_msats,
176			Some(_) => {
177				log_error!(self.logger, "Failed to send payment as the provided offer was denominated in an unsupported currency.");
178				return Err(Error::UnsupportedCurrency);
179			},
180			None => amount_msat,
181		};
182
183		if amount_msat < offer_amount_msat {
184			log_error!(
185				self.logger,
186				"Failed to pay as the given amount needs to be at least the offer amount: required {}msat, gave {}msat.", offer_amount_msat, amount_msat);
187			return Err(Error::InvalidAmount);
188		}
189
190		match self.channel_manager.pay_for_offer(
191			&offer,
192			quantity,
193			Some(amount_msat),
194			payer_note.clone(),
195			payment_id,
196			retry_strategy,
197			max_total_routing_fee_msat,
198		) {
199			Ok(()) => {
200				let payee_pubkey = offer.issuer_signing_pubkey();
201				log_info!(
202					self.logger,
203					"Initiated sending {}msat to {:?}",
204					amount_msat,
205					payee_pubkey
206				);
207
208				let kind = PaymentKind::Bolt12Offer {
209					hash: None,
210					preimage: None,
211					secret: None,
212					offer_id: offer.id(),
213					payer_note: payer_note.map(UntrustedString),
214					quantity,
215				};
216				let payment = PaymentDetails::new(
217					payment_id,
218					kind,
219					Some(amount_msat),
220					None,
221					PaymentDirection::Outbound,
222					PaymentStatus::Pending,
223				);
224				self.payment_store.insert(payment)?;
225
226				Ok(payment_id)
227			},
228			Err(e) => {
229				log_error!(self.logger, "Failed to send payment: {:?}", e);
230				match e {
231					Bolt12SemanticError::DuplicatePaymentId => Err(Error::DuplicatePayment),
232					_ => {
233						let kind = PaymentKind::Bolt12Offer {
234							hash: None,
235							preimage: None,
236							secret: None,
237							offer_id: offer.id(),
238							payer_note: payer_note.map(UntrustedString),
239							quantity,
240						};
241						let payment = PaymentDetails::new(
242							payment_id,
243							kind,
244							Some(amount_msat),
245							None,
246							PaymentDirection::Outbound,
247							PaymentStatus::Failed,
248						);
249						self.payment_store.insert(payment)?;
250						Err(Error::PaymentSendingFailed)
251					},
252				}
253			},
254		}
255	}
256
257	/// Returns a payable offer that can be used to request and receive a payment of the amount
258	/// given.
259	pub fn receive(
260		&self, amount_msat: u64, description: &str, expiry_secs: Option<u32>, quantity: Option<u64>,
261	) -> Result<Offer, Error> {
262		let absolute_expiry = expiry_secs.map(|secs| {
263			(SystemTime::now() + Duration::from_secs(secs as u64))
264				.duration_since(UNIX_EPOCH)
265				.unwrap()
266		});
267
268		let offer_builder =
269			self.channel_manager.create_offer_builder(absolute_expiry).map_err(|e| {
270				log_error!(self.logger, "Failed to create offer builder: {:?}", e);
271				Error::OfferCreationFailed
272			})?;
273
274		let mut offer =
275			offer_builder.amount_msats(amount_msat).description(description.to_string());
276
277		if let Some(qty) = quantity {
278			if qty == 0 {
279				log_error!(self.logger, "Failed to create offer: quantity can't be zero.");
280				return Err(Error::InvalidQuantity);
281			} else {
282				offer = offer.supported_quantity(Quantity::Bounded(NonZeroU64::new(qty).unwrap()))
283			};
284		};
285
286		let finalized_offer = offer.build().map_err(|e| {
287			log_error!(self.logger, "Failed to create offer: {:?}", e);
288			Error::OfferCreationFailed
289		})?;
290
291		Ok(finalized_offer)
292	}
293
294	/// Returns a payable offer that can be used to request and receive a payment for which the
295	/// amount is to be determined by the user, also known as a "zero-amount" offer.
296	pub fn receive_variable_amount(
297		&self, description: &str, expiry_secs: Option<u32>,
298	) -> Result<Offer, Error> {
299		let absolute_expiry = expiry_secs.map(|secs| {
300			(SystemTime::now() + Duration::from_secs(secs as u64))
301				.duration_since(UNIX_EPOCH)
302				.unwrap()
303		});
304
305		let offer_builder =
306			self.channel_manager.create_offer_builder(absolute_expiry).map_err(|e| {
307				log_error!(self.logger, "Failed to create offer builder: {:?}", e);
308				Error::OfferCreationFailed
309			})?;
310		let offer = offer_builder.description(description.to_string()).build().map_err(|e| {
311			log_error!(self.logger, "Failed to create offer: {:?}", e);
312			Error::OfferCreationFailed
313		})?;
314
315		Ok(offer)
316	}
317
318	/// Requests a refund payment for the given [`Refund`].
319	///
320	/// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to
321	/// retrieve the refund).
322	pub fn request_refund_payment(&self, refund: &Refund) -> Result<Bolt12Invoice, Error> {
323		let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| {
324			log_error!(self.logger, "Failed to request refund payment: {:?}", e);
325			Error::InvoiceRequestCreationFailed
326		})?;
327
328		let payment_hash = invoice.payment_hash();
329		let payment_id = PaymentId(payment_hash.0);
330
331		let kind = PaymentKind::Bolt12Refund {
332			hash: Some(payment_hash),
333			preimage: None,
334			secret: None,
335			payer_note: refund.payer_note().map(|note| UntrustedString(note.0.to_string())),
336			quantity: refund.quantity(),
337		};
338
339		let payment = PaymentDetails::new(
340			payment_id,
341			kind,
342			Some(refund.amount_msats()),
343			None,
344			PaymentDirection::Inbound,
345			PaymentStatus::Pending,
346		);
347
348		self.payment_store.insert(payment)?;
349
350		Ok(invoice)
351	}
352
353	/// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given.
354	pub fn initiate_refund(
355		&self, amount_msat: u64, expiry_secs: u32, quantity: Option<u64>,
356		payer_note: Option<String>,
357	) -> Result<Refund, Error> {
358		let mut random_bytes = [0u8; 32];
359		rand::thread_rng().fill_bytes(&mut random_bytes);
360		let payment_id = PaymentId(random_bytes);
361
362		let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64))
363			.duration_since(UNIX_EPOCH)
364			.unwrap();
365		let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT);
366		let max_total_routing_fee_msat = None;
367
368		let mut refund_builder = self
369			.channel_manager
370			.create_refund_builder(
371				amount_msat,
372				absolute_expiry,
373				payment_id,
374				retry_strategy,
375				max_total_routing_fee_msat,
376			)
377			.map_err(|e| {
378				log_error!(self.logger, "Failed to create refund builder: {:?}", e);
379				Error::RefundCreationFailed
380			})?;
381
382		if let Some(qty) = quantity {
383			refund_builder = refund_builder.quantity(qty);
384		}
385
386		if let Some(note) = payer_note.clone() {
387			refund_builder = refund_builder.payer_note(note);
388		}
389
390		let refund = refund_builder.build().map_err(|e| {
391			log_error!(self.logger, "Failed to create refund: {:?}", e);
392			Error::RefundCreationFailed
393		})?;
394
395		log_info!(self.logger, "Offering refund of {}msat", amount_msat);
396
397		let kind = PaymentKind::Bolt12Refund {
398			hash: None,
399			preimage: None,
400			secret: None,
401			payer_note: payer_note.map(|note| UntrustedString(note)),
402			quantity,
403		};
404		let payment = PaymentDetails::new(
405			payment_id,
406			kind,
407			Some(amount_msat),
408			None,
409			PaymentDirection::Outbound,
410			PaymentStatus::Pending,
411		);
412
413		self.payment_store.insert(payment)?;
414
415		Ok(refund)
416	}
417}