Skip to main content

bark/lightning/
pay.rs

1use std::fmt;
2
3use anyhow::Context;
4use bitcoin::Amount;
5use lightning::util::ser::Writeable;
6use lnurllib::lightning_address::LightningAddress;
7use lnurllib::lnurl::LnUrl;
8use log::{info, warn};
9use server_rpc::protos;
10
11use ark::lightning::{
12	offer_request_amount, Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, PaymentHash, Preimage,
13};
14
15use crate::Wallet;
16use crate::WalletVtxo;
17use crate::actions::DriveMode;
18use crate::actions::lightning::pay::ln_pay_action_id;
19use crate::actions::lightning::pay::{
20	Htlcs, LightningSend, LightningSendState, Progress, settle_lightning_send_payment,
21	start_lightning_send,
22};
23use crate::lightning::{lnaddr_invoice, lnurlp_invoice};
24use crate::movement::PaymentMethod;
25
26impl Wallet {
27	/// Returns every in-progress lightning send checkpoint.
28	pub async fn pending_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
29		let mut result = Vec::new();
30		for cp in self.inner.db.get_all_wallet_action_checkpoints().await? {
31			if let Some(ls) = cp.into_lightning_send() {
32				result.push(ls);
33			}
34		}
35		Ok(result)
36	}
37
38	/// Returns every failed lightning payment currently stuck because revocation has failed. When
39	/// HTLC VTXOs approach their expiry, the user should consider starting an exit for each VTXO.
40	/// This will only happen automatically if the [Wallet::allow_lightning_send_to_exit] is called.
41	pub async fn stuck_failed_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
42		let mut result = Vec::new();
43		for send in self.pending_lightning_sends().await? {
44			if send.has_failed_revocation() {
45				result.push(send);
46			}
47		}
48		Ok(result)
49	}
50
51	/// Opts the lightning send identified by `hash` into auto-exiting its
52	/// HTLCs once they approach expiry, after revocation has failed.
53	///
54	/// The flag is persisted on the action checkpoint; the next drive
55	/// (e.g. via [`Self::sync_pending_lightning_send_vtxos`] or
56	/// [`Self::check_lightning_payment`]) picks it up and exits when
57	/// HTLCs are near expiry. See [`LightningSend::has_failed_revocation`].
58	pub async fn allow_lightning_send_to_exit(&self, hash: PaymentHash) -> anyhow::Result<()> {
59		let key = ln_pay_action_id(hash);
60		let _guard = self.inner.lock_manager.try_lock(&key).await
61			.context("Payment operation already in progress for this invoice")?;
62
63		let mut send = self.lightning_send_checkpoint(hash).await?
64			.with_context(|| format!("no in-progress lightning send for payment hash {hash}"))?;
65		send.allow_exit_of_htlcs = true;
66		self.inner.db.upsert_wallet_action_checkpoint(&send.id(), &send.into()).await?;
67		Ok(())
68	}
69
70	/// Returns the VTXOs currently held by any in-progress lightning send.
71	pub async fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
72		let mut vtxos = Vec::new();
73		for send in self.pending_lightning_sends().await? {
74			let ids: Vec<_> = match &send.progress {
75				Progress::Start => send.input_vtxo_ids.clone(),
76				Progress::HtlcReceived(h) => h.vtxo_ids.clone(),
77				Progress::PaymentInitiated(h) => h.vtxo_ids.clone(),
78				Progress::RevocableHtlcs { htlcs, .. } => htlcs.vtxo_ids.clone(),
79				Progress::RevocationStuck { htlcs, .. } => htlcs.vtxo_ids.clone(),
80			};
81			for id in ids {
82				vtxos.push(self.get_vtxo_by_id(id).await?);
83			}
84		}
85		Ok(vtxos)
86	}
87
88	/// Drives every pending lightning send forward by one step (or to
89	/// completion if it's ready). Each action runs to its next park
90	/// independently; errors on one don't stop the others.
91	pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
92		let pending = self.pending_lightning_sends().await?;
93		if pending.is_empty() {
94			return Ok(());
95		}
96		info!("Syncing {} pending lightning sends", pending.len());
97		for send in pending {
98			let id = send.id();
99			if let Err(e) = self.drive_action(send, DriveMode::UntilParkOrDone).await {
100				warn!("Failed to sync lightning send {}: {:#}", id, e);
101			}
102		}
103		Ok(())
104	}
105
106	/// Fetches the current checkpoint for the given payment hash, if any.
107	pub async fn lightning_send_checkpoint(&self, hash: PaymentHash)
108		-> anyhow::Result<Option<LightningSend>>
109	{
110		Ok(self.inner.db.get_wallet_action_checkpoint(&ln_pay_action_id(hash)).await?
111			.and_then(|cp| cp.into_lightning_send()))
112	}
113
114	/// Triage a payment hash: paid, in-progress, or unknown.
115	pub async fn lightning_send_state(&self, hash: PaymentHash)
116		-> anyhow::Result<LightningSendState>
117	{
118		if let Some(paid) = self.inner.db.get_paid_invoice(hash).await? {
119			return Ok(LightningSendState::Paid(paid));
120		}
121		if let Some(cp) = self.lightning_send_checkpoint(hash).await? {
122			return Ok(LightningSendState::InProgress(cp));
123		}
124		Ok(LightningSendState::Unknown)
125	}
126
127	/// Cheap "has this invoice ever been paid?" check.
128	pub async fn is_invoice_paid(&self, hash: PaymentHash) -> anyhow::Result<bool> {
129		Ok(self.inner.db.get_paid_invoice(hash).await?.is_some())
130	}
131
132	/// Drive a lightning send forward (e.g., to settle a pending one
133	/// or revoke a failed one). `wait=true` keeps driving past parks
134	/// until the action terminates. Returns the current state.
135	pub async fn check_lightning_payment(&self, hash: PaymentHash, wait: bool)
136		-> anyhow::Result<LightningSendState>
137	{
138		let send = match self.lightning_send_state(hash).await? {
139			LightningSendState::InProgress(s) => s,
140			s => return Ok(s),
141		};
142
143		let mode = if wait { DriveMode::UntilDone } else { DriveMode::UntilParkOrDone };
144		self.drive_action(send, mode).await?;
145		self.lightning_send_state(hash).await
146	}
147
148	/// Settle a payment using a preimage we already have (e.g. from a
149	/// mailbox notification), skipping the server poll.
150	pub(crate) async fn settle_lightning_send_with_preimage(
151		&self,
152		send: LightningSend,
153		htlcs: Htlcs,
154		preimage: Preimage,
155	) -> anyhow::Result<()> {
156		let payment_hash = send.invoice.payment_hash();
157		if preimage.compute_payment_hash() != payment_hash {
158			bail!("preimage mismatch for payment hash {}", payment_hash);
159		}
160		settle_lightning_send_payment(self, &send, &htlcs, preimage).await?;
161		// Remove the in-progress row now that the paid_invoice record
162		// is the source of truth.
163		self.inner.db.remove_wallet_action_checkpoint(&ln_pay_action_id(payment_hash)).await?;
164		Ok(())
165	}
166
167	/// Pays a Lightning [Invoice] using Ark VTXOs.
168	///
169	/// `wait=true` keeps the call open until the payment settles or
170	/// fails; `wait=false` returns once the payment has been kicked off
171	/// and lets the background sync drive it to completion. Returns the
172	/// parsed [`Invoice`] in either case; callers wanting the preimage
173	/// can look up the settled record via [`Self::lightning_send_state`].
174	pub async fn pay_lightning_invoice<T>(
175		&self,
176		invoice: T,
177		user_amount: Option<Amount>,
178		wait: bool,
179	) -> anyhow::Result<Invoice>
180	where
181		T: TryInto<Invoice>,
182		T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
183	{
184		let invoice = invoice.try_into().context("failed to parse invoice")?;
185		let amount = invoice.get_payment_amount(user_amount)?;
186		info!("Sending bolt11 payment of {} to invoice {}", amount, invoice);
187		self.make_lightning_payment(&invoice, invoice.clone().into(), user_amount, wait).await?;
188		Ok(invoice)
189	}
190
191	/// Same as [`Self::pay_lightning_invoice`] but resolves the invoice
192	/// from a [`LightningAddress`] first.
193	pub async fn pay_lightning_address(
194		&self,
195		addr: &LightningAddress,
196		amount: Amount,
197		comment: Option<impl AsRef<str>>,
198		wait: bool,
199	) -> anyhow::Result<Invoice> {
200		let comment = comment.as_ref();
201		let invoice: Invoice = lnaddr_invoice(addr, amount, comment).await
202			.context("lightning address error")?.into();
203		info!("Sending {} to lightning address {}", amount, addr);
204		self.make_lightning_payment(&invoice, addr.clone().into(), None, wait).await?;
205		info!("Paid invoice {}", invoice);
206		Ok(invoice)
207	}
208
209	/// Same as [`Self::pay_lightning_address`] but resolves the invoice from a
210	/// raw LNURL-pay link (`lnurl1…`) first.
211	///
212	/// Errors if the link decodes to a non-pay LNURL (auth, withdraw, channel).
213	pub async fn pay_lnurl(
214		&self,
215		lnurl: &LnUrl,
216		amount: Amount,
217		comment: Option<impl AsRef<str>>,
218		wait: bool,
219	) -> anyhow::Result<Invoice> {
220		let invoice: Invoice = lnurlp_invoice(&lnurl.url, amount, comment).await
221			.context("lnurl-pay error")?.into();
222		info!("Sending {} to lnurl {}", amount, lnurl);
223		self.make_lightning_payment(&invoice, lnurl.clone().into(), None, wait).await?;
224		info!("Paid invoice {}", invoice);
225		Ok(invoice)
226	}
227
228	/// Attempts to pay the given BOLT12 [`Offer`] using offchain funds.
229	pub async fn pay_lightning_offer(
230		&self,
231		offer: Offer,
232		user_amount: Option<Amount>,
233		wait: bool,
234	) -> anyhow::Result<Invoice> {
235		let (mut srv, _) = self.require_server().await?;
236
237		let offer_bytes = {
238			let mut bytes = Vec::new();
239			offer.write(&mut bytes).context("failed to serialize BOLT12 offer")?;
240			bytes
241		};
242
243		let req = protos::FetchBolt12InvoiceRequest {
244			offer: offer_bytes,
245			amount_sat: user_amount.map(|a| a.to_sat()),
246		};
247
248		// Since the server is the one fetching the invoice, it is the one building the
249		// invoice request. We need to ensure that the amount we authorize here,
250		// before the fetch, and the amount in the invoice we get back match during
251		// validation below.
252		let amount = offer_request_amount(&offer, user_amount)
253			.context("cannot determine the amount to pay for this offer")?;
254		if user_amount.is_some() {
255			info!("Sending bolt12 payment of {} (user amount) to offer {}", amount, offer);
256		} else {
257			info!("Sending bolt12 payment of {} (offer amount) to offer {}", amount, offer);
258		}
259
260		let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
261		let invoice = Bolt12Invoice::try_from(resp.invoice)
262			.map_err(|e| anyhow!("invalid invoice: {:?}", e))?;
263
264		invoice.validate_issuance(&offer, amount)
265			.context("invalid BOLT12 invoice received from offer")?;
266
267		let invoice: Invoice = invoice.into();
268		self.make_lightning_payment(&invoice, offer.into(), Some(amount), wait).await?;
269		info!("Paid invoice: {}", invoice);
270		Ok(invoice)
271	}
272
273	/// Low-level lightning payment primitive. Exposed for
274	/// [`PaymentMethod::Custom`] use cases (e.g. LNURL-pay).
275	pub async fn make_lightning_payment(
276		&self,
277		invoice: &Invoice,
278		original_payment_method: PaymentMethod,
279		user_amount: Option<Amount>,
280		wait: bool,
281	) -> anyhow::Result<()> {
282		if !original_payment_method.is_lightning() && !original_payment_method.is_custom() {
283			bail!("Invalid original payment method for lightning payment");
284		}
285
286		let payment_hash = invoice.payment_hash();
287		let mode = if wait { DriveMode::UntilDone } else { DriveMode::UntilParkOrDone };
288
289		if self.is_invoice_paid(payment_hash).await? {
290			bail!("Invoice has already been paid");
291		}
292
293		let key = ln_pay_action_id(payment_hash);
294		let guard = self.inner.lock_manager.try_lock(&key).await
295			.context("Payment operation already in progress for this invoice")?;
296
297		// Resume an existing checkpoint, or build a fresh send.
298		let action = match self.lightning_send_checkpoint(payment_hash).await? {
299			Some(existing) => existing,
300			None => {
301				let start = start_lightning_send(
302					self, invoice.clone(), user_amount, original_payment_method,
303				).await?;
304
305				self.inner.db.upsert_wallet_action_checkpoint(
306					&start.id(), &start.clone().into()
307				).await?;
308
309				start
310			},
311		};
312
313		self.drive_action_with_guard(action, mode, guard).await
314	}
315}