Skip to main content

bark/lightning/
receive.rs

1use anyhow::Context;
2use bitcoin::Amount;
3use futures::StreamExt;
4use lightning_invoice::Bolt11Invoice;
5use log::{error, info, warn};
6use server_rpc::protos;
7
8use ark::lightning::{Bolt11InvoiceExt, Preimage, PaymentHash};
9
10use crate::Wallet;
11use crate::actions::DriveMode;
12use crate::actions::lightning::receive::{
13	Htlcs, LightningReceive, LightningReceiveState, Progress,
14	ln_recv_action_id, start_lightning_receive
15};
16use crate::movement::MovementStatus;
17use crate::movement::update::MovementUpdate;
18
19impl Wallet {
20	/// Returns every in-progress lightning receive checkpoint, newest first.
21	pub async fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
22		let mut result = Vec::new();
23		for cp in self.inner.db.get_all_wallet_action_checkpoints().await? {
24			if let Some(recv) = cp.into_lightning_receive() {
25				result.push(recv);
26			}
27		}
28		Ok(result)
29	}
30
31	/// Calculates how much balance can currently be claimed via inbound
32	/// lightning payments. Invoices that have not yet been paid (and so hold
33	/// no HTLC vtxos) are not included.
34	pub async fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
35		let mut total = Amount::ZERO;
36		for recv in self.pending_lightning_receives().await? {
37			let vtxo_ids = match &recv.progress {
38				Progress::AwaitingPayment => continue,
39				Progress::HtlcsReady(htlcs) => &htlcs.vtxo_ids,
40				Progress::PreimageRevealed(htlcs) => &htlcs.vtxo_ids,
41				// Claim outputs are destined to another wallet's address.
42				Progress::Delivering(_) => continue,
43			};
44			for id in vtxo_ids {
45				total += self.get_vtxo_by_id(*id).await?.vtxo.amount();
46			}
47		}
48		Ok(total)
49	}
50
51	/// Drives every pending lightning receive forward by one step (or to
52	/// completion if it's ready). Each action runs to its next park
53	/// independently; errors on one don't stop the others.
54	pub async fn sync_pending_lightning_receives(&self) -> anyhow::Result<()> {
55		let pending = self.pending_lightning_receives().await?;
56		if pending.is_empty() {
57			return Ok(());
58		}
59		info!("Syncing {} pending lightning receives", pending.len());
60		for recv in pending {
61			let id = recv.id();
62			if let Err(e) = self.drive_action(recv, DriveMode::UntilParkOrDone).await {
63				warn!("Failed to sync lightning receive {}: {:#}", id, e);
64			}
65		}
66		Ok(())
67	}
68
69	/// Fetches the current checkpoint for the given payment hash, if any.
70	pub async fn lightning_receive_checkpoint(&self, hash: PaymentHash)
71		-> anyhow::Result<Option<LightningReceive>>
72	{
73		Ok(self.inner.db.get_wallet_action_checkpoint(&ln_recv_action_id(hash)).await?
74			.and_then(|cp| cp.into_lightning_receive()))
75	}
76
77	/// Look up the preimage for a receive by payment hash, for witnessing
78	/// an on-chain exit of an HTLC-recv vtxo. Checks the permanent settled
79	/// record (written on both successful claim and exit) first, then any
80	/// in-progress checkpoint.
81	pub(crate) async fn lightning_receive_preimage(&self, hash: PaymentHash)
82		-> anyhow::Result<Option<Preimage>>
83	{
84		if let Some(settled) = self.inner.db.get_settled_lightning_receive(hash).await? {
85			return Ok(Some(settled.preimage));
86		}
87		if let Some(cp) = self.lightning_receive_checkpoint(hash).await? {
88			return Ok(Some(cp.payment_preimage));
89		}
90		Ok(None)
91	}
92
93	/// Triage a payment hash: settled, in-progress, or unknown.
94	pub async fn lightning_receive_state(&self, hash: PaymentHash)
95		-> anyhow::Result<LightningReceiveState>
96	{
97		if let Some(settled) = self.inner.db.get_settled_lightning_receive(hash).await? {
98			return Ok(LightningReceiveState::Settled(settled));
99		}
100		if let Some(cp) = self.lightning_receive_checkpoint(hash).await? {
101			return Ok(LightningReceiveState::InProgress(cp));
102		}
103		bail!("no pending lightning receive found for this payment hash");
104	}
105
106	/// Create, store and return a [`Bolt11Invoice`] for an incoming
107	/// lightning payment.
108	///
109	/// This mints the invoice (and a fresh preimage) and persists an
110	/// `AwaitingPayment` checkpoint; it does not wait for payment. The
111	/// background sync (or an explicit [`Self::try_claim_lightning_receive`])
112	/// drives the receive once an inbound HTLC arrives.
113	///
114	/// An optional `description` is embedded as the invoice memo. An optional
115	/// `token` authenticates the later claim when the wallet owns no spendable
116	/// vtxo to prove ownership with.
117	pub async fn bolt11_invoice(
118		&self,
119		amount: Amount,
120		description: Option<String>,
121		token: Option<String>,
122	) -> anyhow::Result<Bolt11Invoice> {
123		let start = start_lightning_receive(self, amount, description, token, None).await?;
124		self.inner.db.upsert_wallet_action_checkpoint(&start.id(), &start.clone().into()).await?;
125		Ok(start.invoice.clone())
126	}
127
128	/// Create, store and return a [`Bolt11Invoice`] whose claimed Ark VTXO
129	/// will be forwarded to `claim_destination`. A destination owned by
130	/// this wallet is claimed locally instead of via its mailbox.
131	///
132	/// Trust model: the claim arkoor is built directly against
133	/// `claim_destination`'s policy, so the funds are signed to the
134	/// destination the moment they're claimed. This wallet never holds a
135	/// key that can spend them, so it has no custodial control and cannot
136	/// redirect — a malicious or compromised caller of this method cannot
137	/// steal the payment unless they provide a different address than the
138	/// desired end user.
139	///
140	/// It can still strand it: delivering the signed output to the
141	/// destination's mailbox is a separate step that only this wallet can
142	/// perform, and nothing else (not the server, not the destination) can
143	/// discover or recover that output until it does. The delivery is a
144	/// parked action that resumes automatically on restart, so a crash
145	/// recovers on its own — but for as long as this wallet stays offline
146	/// (or simply never delivers), the destination cannot claim funds that
147	/// are already theirs. Forwarding through a third party means trusting
148	/// it to eventually come back online and deliver, not trusting it with
149	/// custody.
150	pub async fn bolt11_invoice_for_address(
151		&self,
152		amount: Amount,
153		claim_destination: ark::Address,
154		description: Option<String>,
155		token: Option<String>,
156	) -> anyhow::Result<Bolt11Invoice> {
157		let start = start_lightning_receive(
158			self, amount, description, token, Some(claim_destination),
159		).await?;
160		self.inner.db.upsert_wallet_action_checkpoint(&start.id(), &start.clone().into()).await?;
161		Ok(start.invoice.clone())
162	}
163
164	/// Cancel a pending lightning receive.
165	///
166	/// Only valid before the server has granted HTLC-recv vtxos (i.e. while
167	/// the receive is still in [`Progress::AwaitingPayment`]): we ask the
168	/// server to cancel the hold invoice and drop our checkpoint. Once HTLCs
169	/// are granted the server has committed, so we refuse — the receive must
170	/// complete or be abandoned on its own near expiry.
171	pub async fn cancel_lightning_receive(&self, hash: PaymentHash) -> anyhow::Result<()> {
172		let key = ln_recv_action_id(hash);
173		// Don't fight a live drive of the same action.
174		let _guard = self.inner.lock_manager.try_lock(&key).await
175			.context("receive operation already in progress for this payment")?;
176
177		let recv = self.lightning_receive_checkpoint(hash).await?
178			.context("no pending lightning receive found for this payment hash")?;
179
180		match recv.progress {
181			Progress::AwaitingPayment => {
182				// Best-effort server cancel: an abandoned hold invoice just
183				// expires server-side, so don't fail the local cancel on error.
184				if let Ok((mut srv, _)) = self.require_server().await {
185					if let Err(e) = srv.client.cancel_lightning_receive(
186						protos::CancelLightningReceiveRequest { payment_hash: hash.to_vec() },
187					).await {
188						warn!("server did not cancel lightning receive {}: {}", hash, e);
189					}
190				}
191				self.stop_wallet_action(&key).await?;
192				Ok(())
193			},
194			Progress::HtlcsReady(_) => {
195				bail!("cannot cancel: HTLCs already granted; the receive will complete \
196					or be abandoned near expiry");
197			},
198			Progress::PreimageRevealed(_) | Progress::Delivering(_) => {
199				bail!("cannot cancel: preimage has already been revealed");
200			},
201		}
202	}
203
204	/// Fall back to exiting a stuck lightning receive's HTLC vtxos on-chain.
205	///
206	/// Once the preimage has been revealed, a failed claim leaves the receive
207	/// pending rather than auto-exiting (see [`Progress::PreimageRevealed`]).
208	/// This lets the caller explicitly publish the preimage on-chain by
209	/// exiting the HTLC vtxos, finishing the receive as failed.
210	///
211	/// Preconditions:
212	/// - the preimage must already have been revealed (the receive is in
213	///   [`Progress::PreimageRevealed`]);
214	/// - the HTLC vtxos must still be present.
215	pub async fn attempt_lightning_receive_exit(
216		&self,
217		payment: impl Into<PaymentHash>,
218	) -> anyhow::Result<()> {
219		let hash = payment.into();
220		let key = ln_recv_action_id(hash);
221		// Don't fight a live drive of the same action.
222		let _guard = self.inner.lock_manager.try_lock(&key).await
223			.context("receive operation already in progress for this payment")?;
224
225		let recv = self.lightning_receive_checkpoint(hash).await?
226			.context("no pending lightning receive found for this payment hash")?;
227
228		let htlcs = match &recv.progress {
229			Progress::PreimageRevealed(htlcs) => htlcs,
230			_ => bail!("preimage must be revealed before attempting to exit"),
231		};
232
233		self.exit_lightning_receive_htlcs(&recv, htlcs).await?;
234
235		// The receive is now terminal: release any locks and drop the
236		// checkpoint row.
237		self.stop_wallet_action(&key).await?;
238		Ok(())
239	}
240
241	/// Escalation: when the preimage has been revealed but the claim cannot
242	/// complete, exit the HTLC vtxos on-chain and finish the movement as
243	/// failed. Driven explicitly by the caller via
244	/// [`Wallet::attempt_lightning_receive_exit`]; the receive is never
245	/// auto-exited.
246	pub async fn exit_lightning_receive_htlcs(
247		&self,
248		recv: &LightningReceive,
249		htlcs: &Htlcs,
250	) -> anyhow::Result<()> {
251		warn!("Exiting HTLC VTXOs for lightning receive {}", recv.payment_hash);
252
253		let mut vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
254		for id in htlcs.vtxo_ids.iter() {
255			vtxos.push(self.get_vtxo_by_id(*id).await?.vtxo);
256		}
257		let vtxo_refs = vtxos.iter().collect::<Vec<_>>();
258		self.inner.exit.start_exit_for_vtxos(&vtxo_refs).await?;
259
260		self.inner.movements.finish_movement_with_update(
261			htlcs.movement_id,
262			MovementStatus::Failed,
263			MovementUpdate::new().exited_vtxos(vtxo_refs),
264		).await?;
265
266		// We only exit once the preimage has been revealed (Claiming phase), so
267		// record it permanently: the exit subsystem needs it to witness the
268		// on-chain HTLC-recv spend, possibly after this checkpoint is gone.
269		let amount = recv.invoice.get_payment_amount(None).unwrap_or(Amount::ZERO);
270		self.inner.db.record_settled_lightning_receive(
271			recv.payment_hash, recv.payment_preimage, &recv.invoice, amount,
272		).await?;
273
274		Ok(())
275	}
276
277	/// Drive a lightning receive forward (e.g. to claim an inbound payment).
278	/// `wait=true` keeps driving past parks until the action terminates.
279	/// Returns the current state.
280	pub async fn try_claim_lightning_receive(&self, hash: PaymentHash, wait: bool)
281		-> anyhow::Result<LightningReceiveState>
282	{
283		if let Some(recv) = self.lightning_receive_checkpoint(hash).await? {
284			let mode = if wait { DriveMode::UntilDone } else { DriveMode::UntilParkOrDone };
285			self.drive_action(recv, mode).await?;
286		}
287		self.lightning_receive_state(hash).await
288	}
289
290	/// Drive every pending lightning receive forward, returning the resulting
291	/// state of each. Errors on individual receives are logged, not returned,
292	/// so one stuck receive doesn't block the others.
293	pub async fn try_claim_all_lightning_receives(&self, wait: bool)
294		-> anyhow::Result<Vec<LightningReceiveState>>
295	{
296		let pending = self.pending_lightning_receives().await?;
297		let total = pending.len();
298
299		if total == 0 {
300			return Ok(vec![]);
301		}
302
303		let results: Vec<_> = tokio_stream::iter(pending)
304			.map(|rcv| async move {
305				self.try_claim_lightning_receive(rcv.invoice.into(), wait).await
306			})
307			.buffer_unordered(3)
308			.collect()
309			.await;
310
311		let mut claimed = vec![];
312		let mut failed = 0;
313
314		for result in results {
315			match result {
316				Ok(receive) => claimed.push(receive),
317				Err(e) => {
318					error!("Error claiming lightning receive: {:#}", e);
319					failed += 1;
320				}
321			}
322		}
323
324		if failed > 0 {
325			info!(
326				"Lightning receive claims: {} succeeded, {} failed out of {} pending",
327				claimed.len(), failed, total
328			);
329		}
330
331		if claimed.is_empty() {
332			anyhow::bail!("All {} lightning receive claim(s) failed", failed);
333		}
334
335		Ok(claimed)
336	}
337}