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