Skip to main content

bark/lightning/
pay.rs

1use std::fmt;
2
3use anyhow::Context;
4use bitcoin::{Amount, SignedAmount};
5use bitcoin::hex::DisplayHex;
6use lightning::util::ser::Writeable;
7use lnurllib::lightning_address::LightningAddress;
8use log::{debug, error, info, trace, warn};
9use server_rpc::protos::{self, lightning_payment_status::PaymentStatus};
10
11use ark::{ProtocolEncoding, VtxoPolicy, musig};
12use ark::arkoor::ArkoorDestination;
13use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
14use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, PaymentHash, Preimage};
15use ark::util::IteratorExt;
16use bitcoin_ext::BlockHeight;
17
18use crate::{Wallet, WalletVtxo};
19use crate::lightning::lnaddr_invoice;
20use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
21use crate::movement::update::MovementUpdate;
22use crate::persist::models::LightningSend;
23use crate::subsystem::{LightningMovement, LightningSendMovement, Subsystem};
24use crate::vtxo::VtxoLockHolder;
25
26const LIGHTNING_PAY_LOCK_PREFIX: &str = "lightning_pay";
27
28impl Wallet {
29	/// Returns each pending lightning payment.
30	pub async fn pending_lightning_sends(&self) -> anyhow::Result<Vec<LightningSend>> {
31		Ok(self.inner.db.get_all_pending_lightning_send().await?)
32	}
33
34	/// Queries the database for any VTXO that is a pending lightning send.
35	pub async fn pending_lightning_send_vtxos(&self) -> anyhow::Result<Vec<WalletVtxo>> {
36		let vtxos = self.inner.db.get_all_pending_lightning_send().await?.into_iter()
37			.flat_map(|pending_lightning_send| pending_lightning_send.htlc_vtxos)
38			.collect::<Vec<_>>();
39
40		Ok(vtxos)
41	}
42
43	/// Syncs pending lightning payments, verifying whether the payment status has changed and
44	/// creating a revocation VTXO if necessary.
45	pub async fn sync_pending_lightning_send_vtxos(&self) -> anyhow::Result<()> {
46		let pending_payments = self.pending_lightning_sends().await?;
47
48		if pending_payments.is_empty() {
49			return Ok(());
50		}
51
52		info!("Syncing {} pending lightning sends", pending_payments.len());
53
54		for payment in pending_payments {
55			let payment_hash = payment.invoice.payment_hash();
56			self.check_lightning_payment(payment_hash, false).await?;
57		}
58
59		Ok(())
60	}
61
62	/// Performs the revocation of HTLC VTXOs associated with a failed Lightning payment.
63	///
64	/// Builds a revocation package, requests server cosign,
65	/// then constructs new spendable VTXOs from server response.
66	///
67	/// Updates wallet database and movement logs to reflect the failed
68	/// payment and new produced VTXOs; removes the pending send record.
69	///
70	/// # Arguments
71	///
72	/// * `payment` - A reference to the [`LightningSend`] representing the failed payment whose
73	///     associated HTLC VTXOs should be revoked.
74	///
75	/// # Errors
76	///
77	/// Returns an error if revocation fails at any step.
78	///
79	/// # Returns
80	///
81	/// Returns `Ok(())` if revocation succeeds and the wallet state is properly updated.
82	async fn process_lightning_revocation(&self, payment: &LightningSend) -> anyhow::Result<()> {
83		let (mut srv, _) = self.require_server().await?;
84		let htlc_ids = payment.htlc_vtxos.iter()
85			.map(|v| v.vtxo.id()).collect::<Vec<_>>();
86
87		debug!("Processing {} HTLC VTXOs for revocation", htlc_ids.len());
88
89		// Hydrate to full so the arkoor builder has the genesis chain.
90		let htlc_vtxos = self.inner.db.get_full_vtxos(&htlc_ids).await
91			.context("failed to hydrate htlc input vtxos for revocation")?;
92
93		let mut secs = Vec::with_capacity(htlc_vtxos.len());
94		let mut pubs = Vec::with_capacity(htlc_vtxos.len());
95		let mut htlc_keypairs = Vec::with_capacity(htlc_vtxos.len());
96		for input in htlc_vtxos.iter() {
97			let keypair = self.get_vtxo_key(input).await?;
98			let (s, p) = musig::nonce_pair(&keypair);
99			secs.push(s);
100			pubs.push(p);
101			htlc_keypairs.push(keypair);
102		}
103
104		let (revocation_keypair, _) = self.derive_store_next_keypair().await?;
105
106		let revocation_claim_policy = VtxoPolicy::new_pubkey(revocation_keypair.public_key());
107		let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
108			htlc_vtxos,
109			revocation_claim_policy,
110		)
111			.context("Failed to construct arkoor package")?
112			.generate_user_nonces(&htlc_keypairs)?;
113
114		let cosign_request = protos::ArkoorPackageCosignRequest::from(
115			builder.cosign_request(),
116		);
117
118		let response = srv.client
119			.request_lightning_pay_htlc_revocation(cosign_request).await
120			.context("server failed to cosign arkoor")?.into_inner();
121
122		let cosign_resp = ArkoorPackageCosignResponse::try_from(response)
123			.context("Failed to parse cosign response from server")?;
124
125		let vtxos = builder
126			.user_cosign(&htlc_keypairs, cosign_resp)
127			.context("Failed to cosign vtxos")?
128			.build_signed_vtxos();
129
130		let mut revoked = Amount::ZERO;
131		for vtxo in &vtxos {
132			debug!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
133			revoked += vtxo.amount();
134		}
135
136		let count = vtxos.len();
137		let effective = -payment.amount.to_signed()? - payment.fee.to_signed()? + revoked.to_signed()?;
138		if effective != SignedAmount::ZERO {
139			warn!("Movement {} should have fee of zero, but got {}: amount = {}, fee = {}, revoked = {}",
140				payment.movement_id, effective, payment.amount, payment.fee, revoked,
141			);
142		}
143		self.inner.movements.finish_movement_with_update(
144			payment.movement_id,
145			MovementStatus::Failed,
146			MovementUpdate::new()
147				.effective_balance(effective)
148				.fee(effective.unsigned_abs())
149				.produced_vtxos(&vtxos)
150		).await?;
151		self.store_spendable_vtxos(&vtxos).await?;
152		self.mark_vtxos_as_spent(&payment.htlc_vtxos).await?;
153
154		self.inner.db.remove_lightning_send(payment.invoice.payment_hash()).await?;
155
156		debug!("Revoked {} HTLC VTXOs", count);
157
158		Ok(())
159	}
160
161	/// Processes the result of a lightning payment by checking the preimage sent by the server and
162	/// completing the payment if successful.
163	///
164	/// Note:
165	/// - That function cannot return an Error if the server provides a valid preimage, meaning
166	/// that if some occur, it is useless to ask for revocation as server wouldn't accept it.
167	/// In that case, it is better to keep the payment pending and try again later
168	///
169	/// # Returns
170	///
171	/// Returns `Ok(Some(Preimage))` if the payment is successfully completed and a preimage is
172	/// received.
173	/// Returns `Ok(None)` if preimage is missing, invalid or does not match the payment hash.
174	/// Returns an `Err` if an error occurs during the payment completion.
175	async fn process_lightning_send_server_preimage(
176		&self,
177		preimage: Option<Vec<u8>>,
178		payment: &LightningSend,
179	) -> anyhow::Result<Option<Preimage>> {
180		let payment_hash = payment.invoice.payment_hash();
181		let preimage_res = preimage
182			.context("preimage is missing")
183			.map(|p| Ok(Preimage::try_from(p)?))
184			.flatten();
185
186		match preimage_res {
187			Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
188				info!("Lightning payment succeeded! Preimage: {}. Payment hash: {}",
189					preimage.as_hex(), payment.invoice.payment_hash().as_hex());
190
191				// Complete the payment
192				self.inner.db.finish_lightning_send(payment_hash, Some(preimage)).await?;
193				self.mark_vtxos_as_spent(&payment.htlc_vtxos).await?;
194				self.inner.movements.finish_movement_with_update(
195					payment.movement_id,
196					MovementStatus::Successful,
197					MovementUpdate::new().metadata([(
198						"payment_preimage".into(),
199						serde_json::to_value(preimage).expect("payment preimage can serde"),
200					)])
201				).await?;
202
203				Ok(Some(preimage))
204			},
205			_ => {
206				error!("Server failed to provide a valid preimage. \
207					Payment hash: {}. Preimage result: {:#?}", payment_hash, preimage_res
208				);
209				Ok(None)
210			}
211		}
212	}
213
214	/// Checks the status of a lightning payment associated with a set of VTXOs, processes the
215	/// payment result and optionally takes appropriate actions based on the payment outcome.
216	///
217	/// # Arguments
218	///
219	/// * `payment_hash` - The [PaymentHash] identifying the lightning payment.
220	/// * `wait`         - If true, asks the server to wait for payment completion (may block longer).
221	///
222	/// # Returns
223	///
224	/// Returns `Ok(Some(LightningSend))` with the current payment status.
225	/// Returns `Ok(None)` if no lightning send is found for the payment hash.
226	/// Returns an `Err` if an error occurs during the process.
227	///
228	/// # Behavior
229	///
230	/// - Validates that all HTLC VTXOs share the same invoice, amount and policy.
231	/// - Sends a request to the Ark server to check the payment status.
232	/// - Depending on the payment status:
233	///   - **Failed**: Revokes the associated VTXOs.
234	///   - **Pending**: Checks if the HTLC has expired based on the tip height. If expired,
235	///     revokes the VTXOs.
236	///   - **Complete**: Extracts the payment preimage, logs the payment, registers movement
237	///     in the database and returns the payment info.
238	pub async fn check_lightning_payment(&self, payment_hash: PaymentHash, wait: bool)
239		-> anyhow::Result<Option<LightningSend>>
240	{
241		trace!("Checking lightning payment status for payment hash: {}", payment_hash);
242
243		// Try to mark this payment as in-flight to prevent concurrent status checks.
244		// This prevents race conditions where multiple concurrent calls could both
245		// attempt to process success/revocation, leading to duplicate operations.
246		let key = format!("{}.{}", LIGHTNING_PAY_LOCK_PREFIX, payment_hash);
247		let _guard = self.inner.lock_manager.try_lock(&key).await
248			.context("Payment operation already in progress for this invoice")?;
249
250		self.check_lightning_payment_inner(payment_hash, wait, None).await
251	}
252
253	/// Like [`check_lightning_payment`](Self::check_lightning_payment) but accepts
254	/// a known preimage (e.g. from a mailbox notification) to skip the server RPC
255	/// when the payment is already known to have succeeded.
256	pub(crate) async fn check_lightning_payment_with_preimage(
257		&self,
258		payment_hash: PaymentHash,
259		known_preimage: Option<Preimage>,
260	) -> anyhow::Result<Option<LightningSend>>
261	{
262		trace!("Checking lightning payment status for payment hash: {}", payment_hash);
263
264		// Try to mark this payment as in-flight to prevent concurrent status checks.
265		// This prevents race conditions where multiple concurrent calls could both
266		// attempt to process success/revocation, leading to duplicate operations.
267		let key = format!("{}.{}", LIGHTNING_PAY_LOCK_PREFIX, 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		self.check_lightning_payment_inner(payment_hash, false, known_preimage).await
272	}
273
274	/// Internal implementation of lightning payment status check after concurrency check.
275	///
276	/// When `known_preimage` is provided (e.g. from a mailbox notification),
277	/// the server RPC is skipped and the preimage is used directly.
278	async fn check_lightning_payment_inner(
279		&self,
280		payment_hash: PaymentHash,
281		wait: bool,
282		known_preimage: Option<Preimage>,
283	) -> anyhow::Result<Option<LightningSend>>
284	{
285		let payment = self.inner.db.get_lightning_send(payment_hash).await?
286			.context("no lightning send found for payment hash")?;
287
288		// If the payment already has a preimage, it was already completed successfully
289		if payment.preimage.is_some() {
290			trace!("Payment already completed with preimage");
291			return Ok(Some(payment));
292		}
293
294		if payment.htlc_vtxos.is_empty() {
295			bail!("No HTLC VTXOs found for payment");
296		}
297
298		let policy = payment.htlc_vtxos.iter()
299			.all_same(|v| v.vtxo.policy())
300			.ok_or(anyhow::anyhow!("All lightning htlc should have the same policy"))?;
301
302		let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
303		if policy.payment_hash != payment_hash {
304			bail!("Payment hash mismatch");
305		}
306
307		// If we already have the preimage (from a mailbox notification),
308		// process it directly without an RPC round-trip to the server.
309		if let Some(preimage) = known_preimage {
310			let preimage_opt = self.process_lightning_send_server_preimage(
311				Some(preimage.to_vec()), &payment,
312			).await?;
313
314			if preimage_opt.is_some() {
315				let updated_payment = self.inner.db.get_lightning_send(payment_hash).await?
316					.context("payment disappeared from database")?;
317				return Ok(Some(updated_payment));
318			}
319			// Fall through to the server RPC path if the preimage was invalid.
320			warn!("Known preimage was invalid, falling back to server RPC");
321		}
322
323		let (mut srv, _) = self.require_server().await?;
324		let req = protos::CheckLightningPaymentRequest {
325			hash: payment_hash.to_vec(),
326			wait,
327		};
328		// NB: we don't early return on server error or bad response because we
329		// don't want it to prevent us from revoking or exiting HTLCs if necessary.
330		let response = srv.client.check_lightning_payment(req).await
331			.map(|r| r.into_inner().payment_status);
332
333		let tip = self.inner.chain.tip().await?;
334		let min_vtxo_expiry = payment.htlc_vtxos.iter()
335			.map(|v| v.vtxo.expiry_height())
336			.min().context("no HTLC VTXOs for expiry check")?;
337		let expired = tip > policy.htlc_expiry
338			|| tip > min_vtxo_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold);
339
340		let should_revoke = match response {
341			Ok(Some(PaymentStatus::Success(status))) => {
342				let preimage_opt = self.process_lightning_send_server_preimage(
343					Some(status.preimage), &payment,
344				).await?;
345
346				if preimage_opt.is_some() {
347					// Re-fetch from DB to get the updated payment with preimage
348					let updated_payment = self.inner.db.get_lightning_send(payment_hash).await?
349						.context("payment disappeared from database")?;
350					return Ok(Some(updated_payment));
351				} else {
352					trace!("Server said payment is complete, but has no valid preimage: {:?}", preimage_opt);
353					expired
354				}
355			},
356			Ok(Some(PaymentStatus::Failed(_))) => {
357				info!("Payment failed, revoking VTXO");
358				true
359			},
360			Ok(Some(PaymentStatus::Pending(_))) => {
361				trace!("Payment is still pending");
362				expired
363			},
364			// bad server response or request error
365			Ok(None) | Err(_) => expired,
366		};
367
368		if should_revoke {
369			debug!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
370				payment_hash, tip, policy.htlc_expiry);
371
372			if let Err(e) = self.process_lightning_revocation(&payment).await {
373				warn!("Failed to revoke VTXO: {}", e);
374
375				// if one of the htlc is about to expire, we exit all of them.
376				// Maybe we want a different behavior here, but we have to decide whether
377				// htlc vtxos revocation is a all or nothing process.
378				if tip > min_vtxo_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
379					warn!("HTLC VTXOs for payment {} are near VTXO expiry, marking to exit", payment_hash);
380
381					let vtxos = payment.htlc_vtxos
382						.iter()
383						.map(|v| v.vtxo.clone())
384						.collect::<Vec<_>>();
385					self.inner.exit.start_exit_for_vtxos(&vtxos).await?;
386
387					let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
388					let effective = -payment.amount.to_signed()? - payment.fee.to_signed()? + exited.to_signed()?;
389					if effective != SignedAmount::ZERO {
390						warn!("Movement {} should have fee of zero, but got {}: amount = {}, fee = {}, exited = {}",
391							payment.movement_id, effective, payment.amount, payment.fee, exited,
392						);
393					}
394					self.inner.movements.finish_movement_with_update(
395						payment.movement_id,
396						MovementStatus::Failed,
397						MovementUpdate::new()
398							.effective_balance(effective)
399							.fee(effective.unsigned_abs())
400							.exited_vtxos(&vtxos)
401					).await?;
402					self.inner.db.finish_lightning_send(payment.invoice.payment_hash(), None).await?;
403				}
404
405				return Err(e)
406			}
407		}
408
409		// Return current payment state from DB (may have been updated by revocation)
410		Ok(self.inner.db.get_lightning_send(payment_hash).await?)
411	}
412
413	/// Pays a Lightning [Invoice] using Ark VTXOs. This is also an out-of-round payment
414	/// so the same [Wallet::send_arkoor_payment] rules apply.
415	///
416	/// # Returns
417	///
418	/// Returns the [Invoice] for which payment was initiated.
419	pub async fn pay_lightning_invoice<T>(
420		&self,
421		invoice: T,
422		user_amount: Option<Amount>,
423	) -> anyhow::Result<LightningSend>
424	where
425		T: TryInto<Invoice>,
426		T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
427	{
428		let invoice = invoice.try_into().context("failed to parse invoice")?;
429		let amount = invoice.get_payment_amount(user_amount)?;
430		info!("Sending bolt11 payment of {} to invoice {}", amount, invoice);
431		self.make_lightning_payment(&invoice, invoice.clone().into(), user_amount).await
432	}
433
434	/// Same as [Wallet::pay_lightning_invoice] but instead it pays a [LightningAddress].
435	pub async fn pay_lightning_address(
436		&self,
437		addr: &LightningAddress,
438		amount: Amount,
439		comment: Option<impl AsRef<str>>,
440	) -> anyhow::Result<LightningSend> {
441		let comment = comment.as_ref();
442		let invoice = lnaddr_invoice(addr, amount, comment).await
443			.context("lightning address error")?;
444		info!("Sending {} to lightning address {}", amount, addr);
445		let ret = self.make_lightning_payment(&invoice.into(), addr.clone().into(), None).await
446			.context("bolt11 payment error")?;
447		info!("Paid invoice {}", ret.invoice);
448		Ok(ret)
449	}
450
451	/// Attempts to pay the given BOLT12 [Offer] using offchain funds.
452	pub async fn pay_lightning_offer(
453		&self,
454		offer: Offer,
455		user_amount: Option<Amount>,
456	) -> anyhow::Result<LightningSend> {
457		let (mut srv, _) = self.require_server().await?;
458
459		let offer_bytes = {
460			let mut bytes = Vec::new();
461			offer.write(&mut bytes).context("failed to serialize BOLT12 offer")?;
462			bytes
463		};
464
465		let req = protos::FetchBolt12InvoiceRequest {
466			offer: offer_bytes,
467			amount_sat: user_amount.map(|a| a.to_sat()),
468		};
469
470		if let Some(amt) = user_amount {
471			info!("Sending bolt12 payment of {} (user amount) to offer {}", amt, offer);
472		} else if let Some(amt) = offer.amount() {
473			info!("Sending bolt12 payment of {:?} (invoice amount) to offer {}", amt, offer);
474		} else {
475			warn!("Paying offer without amount nor user amount provided: {}", offer);
476		}
477
478		let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
479		let invoice = Bolt12Invoice::try_from(resp.invoice)
480			.map_err(|e| anyhow!("invalid invoice: {:?}", e))?;
481
482		invoice.validate_issuance(&offer)
483			.context("invalid BOLT12 invoice received from offer")?;
484
485		let ret = self.make_lightning_payment(&invoice.into(), offer.into(), None).await
486			.context("bolt12 payment error")?;
487		info!("Paid invoice: {}", ret.invoice.to_string());
488
489		Ok(ret)
490	}
491
492	/// Makes a payment using the Lightning Network. This is a low-level primitive to allow for
493	/// more fine-grained control over the payment process. The primary purpose of using this method
494	/// is to support [PaymentMethod::Custom] for other payment use cases such as LNURL-Pay.
495	///
496	/// It's recommended to use the following higher-level functions where suitable:
497	/// - BOLT11: [Wallet::pay_lightning_invoice]
498	/// - BOLT12: [Wallet::pay_lightning_offer]
499	/// - Lightning Address: [Wallet::pay_lightning_address]
500	///
501	/// # Parameters
502	/// - `invoice`: A reference to the BOLT11/BOLT12 invoice to be paid.
503	/// - `original_payment_method`: The payment method that the given invoice was originally
504	///   derived from (e.g., BOLT11, an offer, lightning address). This will appear in the stored
505	///   [Movement](crate::movement::Movement).
506	/// - `user_amount`: An optional custom amount to override the amount specified in the invoice.
507	///   If not provided, the invoice's amount is used.
508	///
509	/// # Returns
510	/// Returns a `LightningSend` representing the successful payment.
511	/// If an error occurs during the process, an `anyhow::Error` is returned.
512	///
513	/// # Errors
514	/// This function can return an error for the following reasons:
515	/// - If the given payment method is not either an officially supported lightning payment method
516	///   or [PaymentMethod::Custom].
517	/// - The `invoice` belongs to a different network than the one configured in the server's
518	///   properties.
519	/// - The `invoice` has already been paid (the payment hash exists in the database).
520	/// - The `invoice` contains an invalid or tampered signature.
521	/// - The wallet doesn't have enough funds to cover the payment.
522	/// - Validation, signing, server or network issues occur.
523	///
524	/// # Notes
525	/// - A movement won't be recorded until we receive an intermediary HTLC VTXO.
526	/// - This is effectively an arkoor payment with an additional HTLC conversion step, so the
527	///   same [Wallet::send_arkoor_payment] rules apply.
528	pub async fn make_lightning_payment(
529		&self,
530		invoice: &Invoice,
531		original_payment_method: PaymentMethod,
532		user_amount: Option<Amount>,
533	) -> anyhow::Result<LightningSend> {
534		if !original_payment_method.is_lightning() && !original_payment_method.is_custom() {
535			bail!("Invalid original payment method for lightning payment");
536		}
537
538		let payment_hash = invoice.payment_hash();
539
540		// Try to mark this payment as in-flight to prevent concurrent attempts.
541		// This prevents a race condition where multiple concurrent calls could all pass
542		// the DB check below before any of them complete, leading to orphaned state.
543		let key = format!("{}.{}", LIGHTNING_PAY_LOCK_PREFIX, payment_hash);
544		let _guard = self.inner.lock_manager.try_lock(&key).await
545			.context("Payment operation already in progress for this invoice")?;
546
547		// Execute the payment, ensuring we remove from inflight set on any exit path
548		self.make_lightning_payment_inner(invoice, original_payment_method, user_amount, payment_hash).await
549	}
550
551	/// Internal implementation of lightning payment after concurrency check.
552	async fn make_lightning_payment_inner(
553		&self,
554		invoice: &Invoice,
555		original_payment_method: PaymentMethod,
556		user_amount: Option<Amount>,
557		payment_hash: PaymentHash,
558	) -> anyhow::Result<LightningSend> {
559		let (mut srv, ark_info) = self.require_server().await?;
560
561		let tip = self.inner.chain.tip().await?;
562
563		let properties = self.inner.db.read_properties().await?.context("Missing config")?;
564		if invoice.network() != properties.network {
565			bail!("Invoice is for wrong network: {}", invoice.network());
566		}
567
568		let lightning_send = self.inner.db.get_lightning_send(payment_hash).await?;
569		if lightning_send.is_some() {
570			bail!("Invoice has already been paid");
571		}
572
573		invoice.check_signature()?;
574
575		let payment_amount = invoice.get_payment_amount(user_amount)?;
576		if payment_amount == Amount::ZERO {
577			bail!("Cannot pay invoice for 0 sats (0 sat invoices are not any-amount invoices)");
578		}
579
580		let (change_keypair, _) = self.derive_store_next_keypair().await?;
581
582		let (inputs, fee) = self.select_vtxos_to_cover_with_fee(
583			payment_amount, |a, v| ark_info.fees.lightning_send.calculate(a, v)
584				.context("fee overflowed"),
585		).await.context("Could not find enough suitable VTXOs to cover lightning payment")?;
586		let total_amount = payment_amount + fee;
587
588		// Hydrate the selected inputs to their full form. We need them full
589		// for arkoor construction below and for registering the chain with
590		// the server.
591		let input_ids = inputs.iter().map(|v| v.id()).collect::<Vec<_>>();
592		let full_inputs = self.inner.db.get_full_vtxos(&input_ids).await
593			.context("failed to hydrate lightning-send input vtxos")?;
594
595		// Ensure that all inputs are fully registered so the server will
596		// accept them.
597		self.register_vtxo_transactions_with_server(&full_inputs).await
598			.context("failed to register lightning-send input vtxo transactions with server")?;
599
600		let mut secs = Vec::with_capacity(inputs.len());
601		let mut pubs = Vec::with_capacity(inputs.len());
602		let mut input_keypairs = Vec::with_capacity(inputs.len());
603		for input in inputs.iter() {
604			let keypair = self.get_vtxo_key(input).await?;
605			let (s, p) = musig::nonce_pair(&keypair);
606			secs.push(s);
607			pubs.push(p);
608			input_keypairs.push(keypair);
609		}
610
611		let expiry = tip + ark_info.htlc_send_expiry_delta as BlockHeight;
612		let policy = VtxoPolicy::new_server_htlc_send(
613			change_keypair.public_key(), invoice.payment_hash(), expiry,
614		);
615
616		let input_amount = inputs.iter().map(|v| v.amount()).sum::<Amount>();
617		let pay_dest = ArkoorDestination { total_amount, policy };
618		let outputs = if input_amount == total_amount {
619			vec![pay_dest]
620		} else {
621			let change_dest = ArkoorDestination {
622				total_amount: input_amount - total_amount,
623				policy: VtxoPolicy::new_pubkey(change_keypair.public_key()),
624			};
625			vec![pay_dest, change_dest]
626		};
627		let builder = ArkoorPackageBuilder::new_with_checkpoints(
628			full_inputs,
629			outputs,
630		)
631			.context("Failed to construct arkoor package")?
632			.generate_user_nonces(&input_keypairs)
633			.context("invalid nb of keypairs")?;
634
635		let package_cosign_request = protos::ArkoorPackageCosignRequest::from(
636			builder.cosign_request(),
637		);
638		let cosign_request = protos::LightningPayHtlcCosignRequest {
639			parts: package_cosign_request.parts,
640		};
641
642		let response = srv.client.request_lightning_pay_htlc_cosign(cosign_request).await
643			.context("htlc request failed")?.into_inner();
644
645		let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
646			.context("Failed to parse cosign response from server")?;
647
648		let vtxos = builder
649			.user_cosign(&input_keypairs, cosign_responses)
650			.context("Failed to cosign vtxos")?
651			.build_signed_vtxos();
652
653		// Ensure both htlcs and change are registered. We must register
654		// change before initiating the lightning payment.
655		self.register_vtxo_transactions_with_server(&vtxos).await?;
656
657		let (htlc_vtxos, change_vtxos) = vtxos.into_iter()
658			.partition::<Vec<_>, _>(|v| matches!(v.policy(), VtxoPolicy::ServerHtlcSend(_)));
659
660		// Validate the new vtxos. They have the same chain anchor.
661		let mut effective_balance = Amount::ZERO;
662		for vtxo in &htlc_vtxos {
663			self.validate_vtxo(vtxo).await?;
664			effective_balance += vtxo.amount();
665		}
666
667		let movement_id = self.inner.movements.new_movement_with_update(
668			Subsystem::LIGHTNING_SEND,
669			LightningSendMovement::Send.to_string(),
670			MovementUpdate::new()
671				.intended_balance(-payment_amount.to_signed()?)
672				.effective_balance(-effective_balance.to_signed()?)
673				.fee(fee)
674				.consumed_vtxos(&inputs)
675				.sent_to([MovementDestination::new(original_payment_method, payment_amount)])
676				.metadata(LightningMovement::metadata(invoice.payment_hash(), &htlc_vtxos, None))
677		).await?;
678
679		let holder = VtxoLockHolder::Movement { id: movement_id };
680		self.store_locked_vtxos(&htlc_vtxos, Some(holder)).await?;
681		self.mark_vtxos_as_spent(&input_ids).await?;
682
683		// Validate the change vtxo. It has the same chain anchor as the last input.
684		for change in &change_vtxos {
685			let last_input = inputs.last().context("no inputs provided")?;
686			let tx = self.inner.chain.get_tx(&last_input.chain_anchor().txid).await?;
687			let tx = tx.with_context(|| {
688				format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
689			})?;
690			change.validate(&tx).context("invalid lightning change vtxo")?;
691			self.store_spendable_vtxos([change]).await?;
692		}
693
694		self.inner.movements.update_movement(
695			movement_id,
696			MovementUpdate::new()
697				.produced_vtxos(change_vtxos)
698				.metadata(LightningMovement::metadata(invoice.payment_hash(), &htlc_vtxos, None))
699		).await?;
700
701		let lightning_send = self.inner.db.store_new_pending_lightning_send(
702			&invoice,
703			payment_amount,
704			fee,
705			&htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(),
706			movement_id,
707		).await?;
708
709		let mailbox_id = self.mailbox_identifier();
710		let req = protos::InitiateLightningPaymentRequest {
711			invoice: invoice.to_string(),
712			htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
713			payment_amount_sat: payment_amount.to_sat(),
714			mailbox_id: Some(mailbox_id.serialize()),
715		};
716
717		srv.client.initiate_lightning_payment(req).await?;
718
719		Ok(lightning_send)
720	}
721}