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