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().convert_vtxo(|vtxo| vtxo.id())
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(Preimage))` if the payment is successfully completed and a preimage is
219	/// received.
220	/// Returns `Ok(None)` for payments still pending, failed payments or if necessary revocation
221	/// or exit processing occurs.
222	/// Returns an `Err` if an error occurs during the process.
223	///
224	/// # Behavior
225	///
226	/// - Validates that all HTLC VTXOs share the same invoice, amount and policy.
227	/// - Sends a request to the Ark server to check the payment status.
228	/// - Depending on the payment status:
229	///   - **Failed**: Revokes the associated VTXOs.
230	///   - **Pending**: Checks if the HTLC has expired based on the tip height. If expired,
231	///     revokes the VTXOs.
232	///   - **Complete**: Extracts the payment preimage, logs the payment, registers movement
233	///     in the database and returns the preimage.
234	pub async fn check_lightning_payment(&self, payment_hash: PaymentHash, wait: bool)
235		-> anyhow::Result<Option<Preimage>>
236	{
237		trace!("Checking lightning payment status for payment hash: {}", payment_hash);
238
239		// Try to mark this payment as in-flight to prevent concurrent status checks.
240		// This prevents race conditions where multiple concurrent calls could both
241		// attempt to process success/revocation, leading to duplicate operations.
242		{
243			let mut inflight = self.inflight_lightning_payments.lock().await;
244			if !inflight.insert(payment_hash) {
245				bail!("Payment operation already in progress for this invoice");
246			}
247		}
248
249		let result = self.check_lightning_payment_inner(payment_hash, wait).await;
250
251		// Always remove from inflight set when done
252		{
253			let mut inflight = self.inflight_lightning_payments.lock().await;
254			inflight.remove(&payment_hash);
255		}
256
257		result
258	}
259
260	/// Internal implementation of lightning payment status check after concurrency check.
261	async fn check_lightning_payment_inner(&self, payment_hash: PaymentHash, wait: bool)
262		-> anyhow::Result<Option<Preimage>>
263	{
264		let (mut srv, _) = self.require_server().await?;
265
266		let payment = self.db.get_lightning_send(payment_hash).await?
267			.context("no lightning send found for payment hash")?;
268
269		// If the payment already has a preimage, it was already completed successfully
270		if let Some(preimage) = payment.preimage {
271			trace!("Payment already completed with preimage: {}", preimage.as_hex());
272			return Ok(Some(preimage));
273		}
274
275		if payment.htlc_vtxos.is_empty() {
276			bail!("No HTLC VTXOs found for payment");
277		}
278
279		let policy = payment.htlc_vtxos.iter()
280			.all_same(|v| v.vtxo.policy())
281			.ok_or(anyhow::anyhow!("All lightning htlc should have the same policy"))?;
282
283		let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
284		if policy.payment_hash != payment_hash {
285			bail!("Payment hash mismatch");
286		}
287
288		let req = protos::CheckLightningPaymentRequest {
289			hash: payment_hash.to_vec(),
290			wait,
291		};
292		// NB: we don't early return on server error or bad response because we
293		// don't want it to prevent us from revoking or exiting HTLCs if necessary.
294		let response = srv.client.check_lightning_payment(req).await
295			.map(|r| r.into_inner().payment_status);
296
297		let tip = self.chain.tip().await?;
298		let min_vtxo_expiry = payment.htlc_vtxos.iter()
299			.map(|v| v.vtxo.expiry_height())
300			.min().context("no HTLC VTXOs for expiry check")?;
301		let expired = tip > policy.htlc_expiry
302			|| tip > min_vtxo_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold);
303
304		let should_revoke = match response {
305			Ok(Some(PaymentStatus::Success(status))) => {
306				let preimage_opt = self.process_lightning_send_server_preimage(
307					Some(status.preimage), &payment,
308				).await?;
309
310				if let Some(preimage) = preimage_opt {
311					return Ok(Some(preimage));
312				} else {
313					trace!("Server said payment is complete, but has no valid preimage: {:?}", preimage_opt);
314					expired
315				}
316			},
317			Ok(Some(PaymentStatus::Failed(_))) => {
318				info!("Payment failed, revoking VTXO");
319				true
320			},
321			Ok(Some(PaymentStatus::Pending(_))) => {
322				trace!("Payment is still pending");
323				expired
324			},
325			// bad server response or request error
326			Ok(None) | Err(_) => expired,
327		};
328
329		if should_revoke {
330			debug!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
331				payment_hash, tip, policy.htlc_expiry);
332
333			if let Err(e) = self.process_lightning_revocation(&payment).await {
334				warn!("Failed to revoke VTXO: {}", e);
335
336				// if one of the htlc is about to expire, we exit all of them.
337				// Maybe we want a different behavior here, but we have to decide whether
338				// htlc vtxos revocation is a all or nothing process.
339				if tip > min_vtxo_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
340					warn!("HTLC VTXOs for payment {} are near VTXO expiry, marking to exit", payment_hash);
341
342					let vtxos = payment.htlc_vtxos
343						.iter()
344						.map(|v| v.vtxo.clone())
345						.collect::<Vec<_>>();
346					self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
347
348					let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
349					let effective = -payment.amount.to_signed()? - payment.fee.to_signed()? + exited.to_signed()?;
350					if effective != SignedAmount::ZERO {
351						warn!("Movement {} should have fee of zero, but got {}: amount = {}, fee = {}, exited = {}",
352							payment.movement_id, effective, payment.amount, payment.fee, exited,
353						);
354					}
355					self.movements.finish_movement_with_update(
356						payment.movement_id,
357						MovementStatus::Failed,
358						MovementUpdate::new()
359							.effective_balance(effective)
360							.fee(effective.unsigned_abs())
361							.exited_vtxos(&vtxos)
362					).await?;
363					self.db.finish_lightning_send(payment.invoice.payment_hash(), None).await?;
364				}
365
366				return Err(e)
367			}
368		}
369
370		Ok(None)
371	}
372
373	/// Pays a Lightning [Invoice] using Ark VTXOs. This is also an out-of-round payment
374	/// so the same [Wallet::send_arkoor_payment] rules apply.
375	///
376	/// # Returns
377	///
378	/// Returns the [Invoice] for which payment was initiated.
379	pub async fn pay_lightning_invoice<T>(
380		&self,
381		invoice: T,
382		user_amount: Option<Amount>,
383	) -> anyhow::Result<LightningSend>
384	where
385		T: TryInto<Invoice>,
386		T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
387	{
388		let invoice = invoice.try_into().context("failed to parse invoice")?;
389		let amount = invoice.get_final_amount(user_amount)?;
390		info!("Sending bolt11 payment of {} to invoice {}", amount, invoice);
391		self.make_lightning_payment(&invoice, invoice.clone().into(), user_amount).await
392	}
393
394	/// Same as [Wallet::pay_lightning_invoice] but instead it pays a [LightningAddress].
395	pub async fn pay_lightning_address(
396		&self,
397		addr: &LightningAddress,
398		amount: Amount,
399		comment: Option<impl AsRef<str>>,
400	) -> anyhow::Result<LightningSend> {
401		let comment = comment.as_ref();
402		let invoice = lnaddr_invoice(addr, amount, comment).await
403			.context("lightning address error")?;
404		info!("Sending {} to lightning address {}", amount, addr);
405		let ret = self.make_lightning_payment(&invoice.into(), addr.clone().into(), None).await
406			.context("bolt11 payment error")?;
407		info!("Paid invoice {}", ret.invoice);
408		Ok(ret)
409	}
410
411	/// Attempts to pay the given BOLT12 [Offer] using offchain funds.
412	pub async fn pay_lightning_offer(
413		&self,
414		offer: Offer,
415		user_amount: Option<Amount>,
416	) -> anyhow::Result<LightningSend> {
417		let (mut srv, _) = self.require_server().await?;
418
419		let offer_bytes = {
420			let mut bytes = Vec::new();
421			offer.write(&mut bytes).context("failed to serialize BOLT12 offer")?;
422			bytes
423		};
424
425		let req = protos::FetchBolt12InvoiceRequest {
426			offer: offer_bytes,
427			amount_sat: user_amount.map(|a| a.to_sat()),
428		};
429
430		if let Some(amt) = user_amount {
431			info!("Sending bolt12 payment of {} (user amount) to offer {}", amt, offer);
432		} else if let Some(amt) = offer.amount() {
433			info!("Sending bolt12 payment of {:?} (invoice amount) to offer {}", amt, offer);
434		} else {
435			warn!("Paying offer without amount nor user amount provided: {}", offer);
436		}
437
438		let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
439		let invoice = Bolt12Invoice::try_from(resp.invoice)
440			.map_err(|e| anyhow!("invalid invoice: {:?}", e))?;
441
442		invoice.validate_issuance(&offer)
443			.context("invalid BOLT12 invoice received from offer")?;
444
445		let ret = self.make_lightning_payment(&invoice.into(), offer.into(), None).await
446			.context("bolt12 payment error")?;
447		info!("Paid invoice: {:?}", ret.invoice);
448
449		Ok(ret)
450	}
451
452	/// Makes a payment using the Lightning Network. This is a low-level primitive to allow for
453	/// more fine-grained control over the payment process. The primary purpose of using this method
454	/// is to support [PaymentMethod::Custom] for other payment use cases such as LNURL-Pay.
455	///
456	/// It's recommended to use the following higher-level functions where suitable:
457	/// - BOLT11: [Wallet::pay_lightning_invoice]
458	/// - BOLT12: [Wallet::pay_lightning_offer]
459	/// - Lightning Address: [Wallet::pay_lightning_address]
460	///
461	/// # Parameters
462	/// - `invoice`: A reference to the BOLT11/BOLT12 invoice to be paid.
463	/// - `original_payment_method`: The payment method that the given invoice was originally
464	///   derived from (e.g., BOLT11, an offer, lightning address). This will appear in the stored
465	///   [Movement](crate::movement::Movement).
466	/// - `user_amount`: An optional custom amount to override the amount specified in the invoice.
467	///   If not provided, the invoice's amount is used.
468	///
469	/// # Returns
470	/// Returns a `LightningSend` representing the successful payment.
471	/// If an error occurs during the process, an `anyhow::Error` is returned.
472	///
473	/// # Errors
474	/// This function can return an error for the following reasons:
475	/// - If the given payment method is not either an officially supported lightning payment method
476	///   or [PaymentMethod::Custom].
477	/// - The `invoice` belongs to a different network than the one configured in the server's
478	///   properties.
479	/// - The `invoice` has already been paid (the payment hash exists in the database).
480	/// - The `invoice` contains an invalid or tampered signature.
481	/// - The wallet doesn't have enough funds to cover the payment.
482	/// - Validation, signing, server or network issues occur.
483	///
484	/// # Notes
485	/// - A movement won't be recorded until we receive an intermediary HTLC VTXO.
486	/// - This is effectively an arkoor payment with an additional HTLC conversion step, so the
487	///   same [Wallet::send_arkoor_payment] rules apply.
488	pub async fn make_lightning_payment(
489		&self,
490		invoice: &Invoice,
491		original_payment_method: PaymentMethod,
492		user_amount: Option<Amount>,
493	) -> anyhow::Result<LightningSend> {
494		if !original_payment_method.is_lightning() && !original_payment_method.is_custom() {
495			bail!("Invalid original payment method for lightning payment");
496		}
497
498		let payment_hash = invoice.payment_hash();
499
500		// Try to mark this payment as in-flight to prevent concurrent attempts.
501		// This prevents a race condition where multiple concurrent calls could all pass
502		// the DB check below before any of them complete, leading to orphaned state.
503		{
504			let mut inflight = self.inflight_lightning_payments.lock().await;
505			if !inflight.insert(payment_hash) {
506				bail!("Payment already in progress for this invoice");
507			}
508		}
509
510		// Execute the payment, ensuring we remove from inflight set on any exit path
511		let result = self.make_lightning_payment_inner(
512			invoice, original_payment_method, user_amount, payment_hash
513		).await;
514
515		// Always remove from inflight set when done
516		{
517			let mut inflight = self.inflight_lightning_payments.lock().await;
518			inflight.remove(&payment_hash);
519		}
520
521		result
522	}
523
524	/// Internal implementation of lightning payment after concurrency check.
525	async fn make_lightning_payment_inner(
526		&self,
527		invoice: &Invoice,
528		original_payment_method: PaymentMethod,
529		user_amount: Option<Amount>,
530		payment_hash: PaymentHash,
531	) -> anyhow::Result<LightningSend> {
532		let (mut srv, ark_info) = self.require_server().await?;
533
534		let tip = self.chain.tip().await?;
535
536		let properties = self.db.read_properties().await?.context("Missing config")?;
537		if invoice.network() != properties.network {
538			bail!("Invoice is for wrong network: {}", invoice.network());
539		}
540
541		let lightning_send = self.db.get_lightning_send(payment_hash).await?;
542		if lightning_send.is_some() {
543			bail!("Invoice has already been paid");
544		}
545
546		invoice.check_signature()?;
547
548		let amount = invoice.get_final_amount(user_amount)?;
549		if amount == Amount::ZERO {
550			bail!("Cannot pay invoice for 0 sats (0 sat invoices are not any-amount invoices)");
551		}
552
553		let (user_keypair, _) = self.derive_store_next_keypair().await?;
554
555		let (inputs, fee) = self.select_vtxos_to_cover_with_fee(
556			amount, |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
557		).await.context("Could not find enough suitable VTXOs to cover lightning payment")?;
558		let total_amount = amount + fee;
559
560		let mut secs = Vec::with_capacity(inputs.len());
561		let mut pubs = Vec::with_capacity(inputs.len());
562		let mut input_keypairs = Vec::with_capacity(inputs.len());
563		let mut input_ids = Vec::with_capacity(inputs.len());
564		for input in inputs.iter() {
565			let keypair = self.get_vtxo_key(input).await?;
566			let (s, p) = musig::nonce_pair(&keypair);
567			secs.push(s);
568			pubs.push(p);
569			input_keypairs.push(keypair);
570			input_ids.push(input.id());
571		}
572
573		let expiry = tip + ark_info.htlc_send_expiry_delta as BlockHeight;
574		let policy = VtxoPolicy::new_server_htlc_send(
575			user_keypair.public_key(), invoice.payment_hash(), expiry,
576		);
577
578		let input_amount = inputs.iter().map(|v| v.amount()).sum::<Amount>();
579		let pay_dest = ArkoorDestination { total_amount, policy };
580		let outputs = if input_amount == total_amount {
581			vec![pay_dest]
582		} else {
583			let change_dest = ArkoorDestination {
584				total_amount: input_amount - total_amount,
585				policy: VtxoPolicy::new_pubkey(user_keypair.public_key()),
586			};
587			vec![pay_dest, change_dest]
588		};
589		let builder = ArkoorPackageBuilder::new_with_checkpoints(
590			inputs.iter().map(|v| &v.vtxo).cloned(),
591			outputs,
592		)
593			.context("Failed to construct arkoor package")?
594			.generate_user_nonces(&input_keypairs)
595			.context("invalid nb of keypairs")?;
596
597		let cosign_request = protos::LightningPayHtlcCosignRequest {
598			invoice: invoice.to_string(),
599			payment_amount_sat: amount.to_sat(),
600			parts: builder.cosign_request()
601				.convert_vtxo(|vtxo| vtxo.id())
602				.requests.into_iter()
603				.map(|r| r.into()).collect(),
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}