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