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