1use std::time::Duration;
15
16use anyhow::Context;
17use bitcoin::hex::DisplayHex;
18use bitcoin::secp256k1::PublicKey;
19use bitcoin::{Amount, SignedAmount};
20use log::{debug, error, info, trace, warn};
21
22use ark::arkoor::ArkoorDestination;
23use ark::arkoor::package::{ArkoorPackageBuilder, ArkoorPackageCosignResponse};
24use ark::lightning::{Invoice, PaymentHash, PaymentStatus, Preimage};
25use ark::mailbox::MailboxIdentifier;
26use ark::util::IteratorExt;
27use ark::{ProtocolEncoding, VtxoId, VtxoPolicy};
28use bitcoin_ext::BlockHeight;
29use server_rpc::protos::{self, lightning_payment_status};
30
31use crate::Wallet;
32use crate::actions::{Advance, AdvanceError, WalletAction, WalletActionId, park_with_backoff};
33use crate::movement::update::MovementUpdate;
34use crate::movement::{MovementDestination, MovementId, MovementStatus, PaymentMethod};
35use crate::persist::models::PaidInvoice;
36use crate::subsystem::{LightningMovement, LightningSendMovement, Subsystem};
37use crate::vtxo::VtxoLockHolder;
38
39const LN_PAY_NAMESPACE: &str = "ln_pay";
40
41pub(crate) fn ln_pay_action_id(payment_hash: PaymentHash) -> WalletActionId {
42 format!("{LN_PAY_NAMESPACE}.{payment_hash}")
43}
44
45#[derive(Debug, Clone, PartialEq)]
51pub enum LightningSendState {
52 Unknown,
53 InProgress(LightningSend),
54 Paid(PaidInvoice),
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct LightningSend {
61 pub invoice: Invoice,
63 pub original_payment_method: PaymentMethod,
64 pub input_vtxo_ids: Vec<VtxoId>,
65 pub payment_amount: Amount,
66 pub fee: Amount,
67
68 pub htlc_key: PublicKey,
71 pub htlc_expiry: BlockHeight,
72
73 #[serde(default)]
77 pub movement_id: Option<MovementId>,
78
79 #[serde(default)]
82 pub revocation_key: Option<PublicKey>,
83
84 pub progress: Progress,
86 pub allow_exit_of_htlcs: bool,
88}
89
90impl LightningSend {
91 pub fn id(&self) -> WalletActionId {
92 ln_pay_action_id(self.invoice.payment_hash())
93 }
94
95 pub fn total_amount(&self) -> Amount {
96 self.payment_amount + self.fee
97 }
98
99 pub async fn is_htlc_near_expiry(&self, wallet: &Wallet) -> anyhow::Result<bool> {
102 let tip = wallet.inner.chain.tip().await?;
103 Ok(tip > self.htlc_expiry
104 .saturating_sub(wallet.config().vtxo_refresh_expiry_threshold))
105 }
106
107 pub fn has_failed_revocation(&self) -> bool {
111 matches!(self.progress, Progress::RevocationStuck { .. })
112 }
113}
114
115#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
116#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
117impl WalletAction for LightningSend {
118 fn id(&self) -> WalletActionId { LightningSend::id(self) }
119
120 async fn advance(self, wallet: &Wallet) -> Result<Advance<Self>, AdvanceError> {
121 let new_progress = match self.progress.clone() {
122 Progress::Start => {
123 let htlcs = request_lightning_send_htlcs(wallet, &self).await?;
124 Progress::HtlcReceived(htlcs)
125 },
126 Progress::HtlcReceived(htlcs) => {
127 initiate_lightning_send_payment(wallet, &self, &htlcs).await?;
128 Progress::PaymentInitiated(htlcs)
129 },
130 Progress::PaymentInitiated(htlcs) => {
131 let wait = false;
132 match check_lightning_send_payment_status(
133 wallet, &self, &htlcs, wait,
134 ).await? {
135 PaymentStatus::Success(preimage) => {
136 settle_lightning_send_payment(wallet, &self, &htlcs, preimage).await?;
137 return Ok(Advance::Done);
138 },
139 PaymentStatus::Failed => {
140 let revocation = fail_lightning_send_payment(wallet, &self).await?;
141 Progress::RevocableHtlcs { htlcs, revocation }
142 },
143 PaymentStatus::Pending => {
144 if self.is_htlc_near_expiry(wallet).await? {
145 let revocation = fail_lightning_send_payment(wallet, &self).await?;
146 Progress::RevocableHtlcs { htlcs, revocation }
147 } else {
148 return Ok(Advance::Park {
149 state: LightningSend {
150 progress: Progress::PaymentInitiated(htlcs),
151 ..self
152 },
153 wake_after: Some(PAYMENT_PENDING_POLL_INTERVAL),
154 error: None,
155 });
156 }
157 },
158 }
159 },
160 Progress::RevocableHtlcs { htlcs, revocation } |
161 Progress::RevocationStuck { htlcs, revocation } => {
162 handle_lightning_send_htlcs_revocation(wallet, &self, &htlcs, &revocation).await?;
163 return Ok(Advance::Done);
164 },
165 };
166
167 Ok(Advance::Next(LightningSend { progress: new_progress, ..self }))
168 }
169
170 async fn on_retry(self, wallet: &Wallet, retries: u32, _error: AdvanceError)
171 -> anyhow::Result<Advance<Self>>
172 {
173 match self.progress.clone() {
174 Progress::Start => {
175 if self.is_htlc_near_expiry(wallet).await? {
176 let err = anyhow!("Could not start lightning send and HTLCs are near expiry");
177 return Ok(Advance::Failed(err));
178 }
179 },
180 Progress::HtlcReceived(htlcs) |
181 Progress::PaymentInitiated(htlcs) => {
182 if self.is_htlc_near_expiry(wallet).await? {
183 let revocation = fail_lightning_send_payment(wallet, &self).await?;
184 let next = LightningSend {
185 progress: Progress::RevocableHtlcs { htlcs, revocation },
186 ..self
187 };
188 return Ok(Advance::Next(next));
189 }
190 },
191 Progress::RevocableHtlcs { htlcs, revocation } => {
192 warn!("We could not revoke HTLCs, will continue retrying but the attempt will be marked as such");
193 let next = LightningSend {
194 progress: Progress::RevocationStuck { htlcs, revocation },
195 ..self
196 };
197 return Ok(Advance::Next(next));
198 },
199 Progress::RevocationStuck { htlcs, .. } => {
200 if self.allow_exit_of_htlcs && self.is_htlc_near_expiry(wallet).await? {
201 exit_lightning_send_htlcs(wallet, &self, &htlcs).await?;
202 return Ok(Advance::Done);
203 }
204 },
206 }
207
208 Ok(park_with_backoff(self, retries))
209 }
210
211 async fn on_rejection(self, wallet: &Wallet, error: AdvanceError) -> anyhow::Result<Advance<Self>> {
212 match self.progress.clone() {
213 Progress::Start => {
218 let id = self.id();
219 error!("Could not start lightning send {}: {:?}", id, error);
220 if let Err(cancel_err) = wallet.stop_wallet_action(&id).await {
221 warn!("could not cancel start-phase lightning send {}: {:#}", id, cancel_err);
222 }
223 Ok(Advance::Failed(error.into()))
224 },
225 Progress::HtlcReceived(htlcs) |
226 Progress::PaymentInitiated(htlcs) => {
227 let revocation = fail_lightning_send_payment(wallet, &self).await?;
228 let next = LightningSend {
229 progress: Progress::RevocableHtlcs { htlcs, revocation },
230 ..self
231 };
232 Ok(Advance::Next(next))
233 },
234 Progress::RevocableHtlcs { htlcs, revocation } => {
235 warn!("We could not revoke HTLCs, will continue retrying but the attempt will be marked as such");
236 let next = LightningSend {
237 progress: Progress::RevocationStuck { htlcs, revocation },
238 ..self
239 };
240 Ok(Advance::Next(next))
241 },
242 Progress::RevocationStuck { htlcs, .. } => {
243 if self.allow_exit_of_htlcs && self.is_htlc_near_expiry(wallet).await? {
244 exit_lightning_send_htlcs(wallet, &self, &htlcs).await?;
245 return Ok(Advance::Failed(anyhow!("Server refused to revoke HTLCs, exiting")));
246 }
247 Ok(Advance::Park { state: self, wake_after: None, error: Some(error) })
251 },
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260pub enum Progress {
261 Start,
263 HtlcReceived(Htlcs),
265 PaymentInitiated(Htlcs),
267 RevocableHtlcs { htlcs: Htlcs, revocation: Revocation },
269 RevocationStuck { htlcs: Htlcs, revocation: Revocation },
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct Htlcs {
280 pub vtxo_ids: Vec<VtxoId>,
281 #[serde(with = "ark::encode::serde")]
282 pub mailbox_id: MailboxIdentifier,
283 pub movement_id: MovementId,
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290pub struct Revocation {
291 pub key: PublicKey,
292}
293
294const PAYMENT_PENDING_POLL_INTERVAL: Duration = Duration::from_secs(2);
296
297pub(crate) async fn start_lightning_send(
304 wallet: &Wallet,
305 invoice: Invoice,
306 user_amount: Option<Amount>,
307 original_payment_method: PaymentMethod,
308) -> anyhow::Result<LightningSend> {
309 let (_, ark_info) = wallet.require_server().await?;
310 let tip = wallet.inner.chain.tip().await?;
311
312 let properties = wallet.inner.db.read_properties().await?.context("Missing config")?;
313 if invoice.network() != properties.network {
314 bail!("Invoice is for wrong network: {}", invoice.network());
315 }
316
317 invoice.check_signature()?;
318
319 let payment_amount = invoice.get_payment_amount(user_amount)?;
320 if payment_amount == Amount::ZERO {
321 bail!("Cannot pay invoice for 0 sats (0 sat invoices are not any-amount invoices)");
322 }
323
324 let (inputs, fee) = wallet.select_any_vtxos_to_cover_with_fee(
325 payment_amount,
326 |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
327 ).await.context("Could not find enough suitable VTXOs to cover lightning payment")?;
328
329 let action_id = ln_pay_action_id(invoice.payment_hash());
330 wallet.lock_vtxos(
331 &inputs,
332 Some(crate::vtxo::VtxoLockHolder::Action { id: action_id }),
333 ).await?;
334
335 let (change_keypair, _) = wallet.derive_store_next_keypair().await?;
336 let (revocation_keypair, _) = wallet.derive_store_next_keypair().await?;
337
338 let htlc_expiry = tip + ark_info.htlc_send_expiry_delta as BlockHeight;
339
340 let movement_id = wallet.inner.movements.new_movement_with_update(
341 Subsystem::LIGHTNING_SEND,
342 LightningSendMovement::Send.to_string(),
343 MovementUpdate::new()
344 .intended_balance(-payment_amount.to_signed().context("payment amount out of range")?)
345 .fee(fee)
346 .consumed_vtxos(&inputs)
347 .sent_to([MovementDestination::new(original_payment_method.clone(), payment_amount)])
348 .metadata(LightningMovement::metadata(invoice.payment_hash(), Vec::<VtxoId>::new(), None))
349 ).await.context("failed to create lightning-send movement")?;
350
351 Ok(LightningSend {
352 invoice,
353 original_payment_method,
354 input_vtxo_ids: inputs.iter().map(|v| v.id()).collect(),
355 payment_amount,
356 fee,
357 htlc_key: change_keypair.public_key(),
358 htlc_expiry,
359 movement_id: Some(movement_id),
360 revocation_key: Some(revocation_keypair.public_key()),
361 allow_exit_of_htlcs: false,
362 progress: Progress::Start,
363 })
364}
365
366pub(crate) async fn request_lightning_send_htlcs(
374 wallet: &Wallet,
375 send: &LightningSend,
376) -> Result<Htlcs, AdvanceError> {
377 let (mut srv, _) = wallet.require_server().await?;
378
379 let full_inputs = wallet.inner.db.get_full_vtxos(&send.input_vtxo_ids).await
380 .context("failed to hydrate lightning-send input vtxos")?;
381
382 wallet.register_vtxo_transactions_with_server(&full_inputs).await
384 .context("failed to register lightning-send input vtxo transactions with server")?;
385
386 let mut input_keypairs = Vec::with_capacity(full_inputs.len());
387 for input in full_inputs.iter() {
388 input_keypairs.push(wallet.get_vtxo_key(input).await?);
389 }
390
391 let policy = VtxoPolicy::new_server_htlc_send(
392 send.htlc_key, send.invoice.payment_hash(), send.htlc_expiry,
393 );
394 let total_amount = send.total_amount();
395 let input_amount = full_inputs.iter().map(|v| v.amount()).sum::<Amount>();
396 let pay_dest = ArkoorDestination { total_amount, policy };
397 let outputs = if input_amount == total_amount {
398 vec![pay_dest]
399 } else {
400 let change_dest = ArkoorDestination {
401 total_amount: input_amount - total_amount,
402 policy: VtxoPolicy::new_pubkey(send.htlc_key),
403 };
404 vec![pay_dest, change_dest]
405 };
406
407 let builder = ArkoorPackageBuilder::new_with_checkpoints(
408 full_inputs.clone(),
409 outputs,
410 )
411 .context("Failed to construct arkoor package")?
412 .generate_user_nonces(&input_keypairs)
413 .context("invalid nb of keypairs")?;
414
415 let cosign_request = protos::LightningPayHtlcCosignRequest {
416 parts: protos::ArkoorPackageCosignRequest::from(builder.cosign_request()).parts,
417 };
418 let response = srv.client.request_lightning_pay_htlc_cosign(cosign_request).await
419 .map_err(AdvanceError::Server)?.into_inner();
420 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
421 .context("Failed to parse cosign response from server")?;
422
423 let vtxos = builder
424 .user_cosign(&input_keypairs, cosign_responses)
425 .context("Failed to cosign vtxos")?
426 .build_signed_vtxos();
427
428 let (htlc_vtxos, change_vtxos) = vtxos.clone().into_iter()
429 .partition::<Vec<_>, _>(|v| matches!(v.policy(), VtxoPolicy::ServerHtlcSend(_)));
430
431 let mut effective_balance = Amount::ZERO;
432 for vtxo in &htlc_vtxos {
433 wallet.validate_vtxo(vtxo).await.map_err(AdvanceError::Vtxo)?;
434 effective_balance += vtxo.amount();
435 }
436 for change in &change_vtxos {
437 let last_input = full_inputs.last().context("no inputs provided")?;
438 let tx = wallet.inner.chain.get_tx(&last_input.chain_anchor().txid).await?;
439 let tx = tx.with_context(|| format!(
440 "input vtxo chain anchor not found for lightning change vtxo: {}",
441 last_input.chain_anchor().txid,
442 ))?;
443 change.validate(&tx).context("invalid lightning change vtxo")?;
444 }
445
446 if let Err(e) = wallet.register_vtxo_transactions_with_server(&vtxos).await {
447 warn!("failed to register lightning-send output vtxo transactions with server: {:#}", e);
448 }
449
450 let movement_id = match send.movement_id {
457 Some(id) => id,
458 None => wallet.inner.movements.new_movement_with_update(
459 Subsystem::LIGHTNING_SEND,
460 LightningSendMovement::Send.to_string(),
461 MovementUpdate::new()
462 .intended_balance(-send.payment_amount.to_signed().context("payment amount out of range")?)
463 .fee(send.fee)
464 .consumed_vtxos(&full_inputs)
465 .sent_to([MovementDestination::new(send.original_payment_method.clone(), send.payment_amount)])
466 ).await.context("failed to create lightning-send movement")?,
467 };
468 wallet.store_locked_vtxos(
469 &htlc_vtxos,
470 Some(VtxoLockHolder::Action { id: send.id() })
471 ).await?;
472 wallet.mark_vtxos_as_spent(&send.input_vtxo_ids).await?;
473 wallet.store_spendable_vtxos(&change_vtxos).await?;
474 wallet.inner.movements.update_movement(
475 movement_id,
476 MovementUpdate::new()
477 .effective_balance(-effective_balance.to_signed().context("effective balance out of range")?)
478 .produced_vtxos(change_vtxos)
479 .metadata(LightningMovement::metadata(send.invoice.payment_hash(), &htlc_vtxos, None))
480 ).await.context("failed to update lightning-send movement")?;
481
482 let mut vtxo_ids = htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
484 vtxo_ids.sort();
485
486 Ok(Htlcs {
487 vtxo_ids,
488 mailbox_id: wallet.mailbox_identifier(),
489 movement_id,
490 })
491}
492
493pub(crate) async fn initiate_lightning_send_payment(
497 wallet: &Wallet,
498 send: &LightningSend,
499 htlcs: &Htlcs,
500) -> Result<(), AdvanceError> {
501 let (mut srv, _) = wallet.require_server().await?;
502
503 let req = protos::InitiateLightningPaymentRequest {
504 invoice: send.invoice.to_string(),
505 htlc_vtxo_ids: htlcs.vtxo_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
506 payment_amount_sat: send.payment_amount.to_sat(),
507 mailbox_id: Some(htlcs.mailbox_id.serialize()),
508 };
509 srv.client.initiate_lightning_payment(req).await
510 .map_err(AdvanceError::Server)?;
511
512 Ok(())
513}
514
515pub(crate) async fn check_lightning_send_payment_status(
519 wallet: &Wallet,
520 send: &LightningSend,
521 htlcs: &Htlcs,
522 wait: bool,
523) -> anyhow::Result<PaymentStatus> {
524 let (mut srv, _) = wallet.require_server().await?;
525 let payment_hash = send.invoice.payment_hash();
526
527 let mut htlc_vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
528 for id in htlcs.vtxo_ids.iter() {
529 htlc_vtxos.push(wallet.get_vtxo_by_id(*id).await?);
530 }
531
532 let policy = htlc_vtxos.iter()
533 .all_same(|v| v.vtxo.policy())
534 .context("All lightning htlc should have the same policy")?;
535 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
536 if policy.payment_hash != payment_hash {
537 bail!("Payment hash mismatch on stored HTLC policy");
538 }
539
540 let tip = wallet.inner.chain.tip().await?;
541 let expired = tip > policy.htlc_expiry;
542 let pending_status = if expired { PaymentStatus::Failed } else { PaymentStatus::Pending };
543
544 let req = protos::CheckLightningPaymentRequest {
545 hash: payment_hash.to_vec(),
546 wait,
547 };
548 let response = srv.client.check_lightning_payment(req).await
551 .map(|r| r.into_inner().payment_status);
552
553 match response {
554 Ok(Some(lightning_payment_status::PaymentStatus::Success(s))) => {
555 match Preimage::try_from(s.preimage) {
556 Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
557 Ok(PaymentStatus::Success(preimage))
558 },
559 other => {
560 error!(
561 "Server reported success but returned an invalid preimage for {}: {:?}",
562 payment_hash, other,
563 );
564 Ok(pending_status)
565 },
566 }
567 },
568 Ok(Some(lightning_payment_status::PaymentStatus::Failed(_))) => {
569 Ok(PaymentStatus::Failed)
570 },
571 Ok(Some(lightning_payment_status::PaymentStatus::Pending(_))) => {
572 trace!("Payment {} is still pending", payment_hash);
573 Ok(pending_status)
574 },
575 Ok(None) | Err(_) => Ok(pending_status),
576 }
577}
578
579pub(crate) async fn settle_lightning_send_payment(
582 wallet: &Wallet,
583 send: &LightningSend,
584 htlcs: &Htlcs,
585 preimage: Preimage,
586) -> anyhow::Result<()> {
587 let payment_hash = send.invoice.payment_hash();
588 if preimage.compute_payment_hash() != payment_hash {
589 bail!("preimage does not match payment hash {}", payment_hash);
590 }
591 info!(
592 "Lightning payment succeeded! Preimage: {}. Payment hash: {}",
593 preimage.as_hex(), payment_hash.as_hex(),
594 );
595
596 wallet.inner.db.record_paid_invoice(payment_hash, preimage).await?;
597 wallet.mark_vtxos_as_spent(&htlcs.vtxo_ids).await?;
598 wallet.inner.movements.finish_movement_with_update(
599 htlcs.movement_id,
600 MovementStatus::Successful,
601 MovementUpdate::new().metadata([(
602 "payment_preimage".into(),
603 serde_json::to_value(preimage).expect("payment preimage can serde"),
604 )]),
605 ).await?;
606
607 Ok(())
608}
609
610pub(crate) async fn fail_lightning_send_payment(
614 wallet: &Wallet,
615 send: &LightningSend,
616) -> anyhow::Result<Revocation> {
617 info!("Lightning payment {} failed, preparing to revoke", send.invoice.payment_hash());
618 let key = match send.revocation_key {
623 Some(key) => key,
624 None => wallet.derive_store_next_keypair().await?.0.public_key(),
625 };
626 Ok(Revocation { key })
627}
628
629pub(crate) async fn revoke_lightning_send_htlcs(
633 wallet: &Wallet,
634 send: &LightningSend,
635 htlcs: &Htlcs,
636 revocation: &Revocation,
637) -> Result<(), AdvanceError> {
638 let (mut srv, _) = wallet.require_server().await?;
639
640 debug!("Revoking {} HTLC vtxos for payment {}",
641 htlcs.vtxo_ids.len(), send.invoice.payment_hash());
642
643 let mut htlc_keypairs = Vec::with_capacity(htlcs.vtxo_ids.len());
644 let mut htlc_vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
645 for id in htlcs.vtxo_ids.iter() {
646 let vtxo = wallet.inner.db.get_full_vtxo(*id).await?
647 .with_context(|| format!("htlc vtxo with id {} not found", id))?;
648 htlc_keypairs.push(wallet.get_vtxo_key(&vtxo).await?);
649 htlc_vtxos.push(vtxo);
650 }
651
652 let revocation_claim_policy = VtxoPolicy::new_pubkey(revocation.key);
653 let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
654 htlc_vtxos.iter().cloned(),
655 revocation_claim_policy,
656 )
657 .context("Failed to construct arkoor package")?
658 .generate_user_nonces(&htlc_keypairs)
659 .context("failed to generate user nonces")?;
660
661 let cosign_request = protos::ArkoorPackageCosignRequest::from(builder.cosign_request());
662 let response = srv.client
663 .request_lightning_pay_htlc_revocation(cosign_request).await
664 .map_err(AdvanceError::Server)?.into_inner();
665 let cosign_resp = ArkoorPackageCosignResponse::try_from(response)
666 .context("Failed to parse cosign response from server")?;
667
668 let vtxos = builder
669 .user_cosign(&htlc_keypairs, cosign_resp)
670 .context("Failed to cosign vtxos")?
671 .build_signed_vtxos();
672
673 if let Err(e) = wallet.register_vtxo_transactions_with_server(&vtxos).await {
675 warn!("failed to register lightning-send revocation vtxo transactions with server: {:#}", e);
676 }
677
678 let revoked = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
679 let effective = -send.total_amount().to_signed().context("total amount out of range")? +
680 revoked.to_signed().context("revoked amount out of range")?;
681 if effective != SignedAmount::ZERO {
682 warn!(
683 "Movement {} should have fee of zero, but got {}: total = {}, revoked = {}",
684 htlcs.movement_id, effective, send.total_amount(), revoked,
685 );
686 }
687 wallet.inner.movements.finish_movement_with_update(
688 htlcs.movement_id,
689 MovementStatus::Failed,
690 MovementUpdate::new()
691 .effective_balance(effective)
692 .fee(effective.unsigned_abs())
693 .produced_vtxos(&vtxos),
694 ).await.context("failed to update movement")?;
695 wallet.store_spendable_vtxos(&vtxos).await?;
696 wallet.mark_vtxos_as_spent(&htlc_vtxos).await?;
697
698 Ok(())
699}
700
701pub(crate) async fn exit_lightning_send_htlcs(
705 wallet: &Wallet,
706 send: &LightningSend,
707 htlcs: &Htlcs,
708) -> anyhow::Result<()> {
709 let payment_hash = send.invoice.payment_hash();
710 warn!("HTLC VTXOs for payment {} are near expiry, marking to exit", payment_hash);
711
712 let mut vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
713 for id in htlcs.vtxo_ids.iter() {
714 vtxos.push(wallet.get_vtxo_by_id(*id).await?.vtxo);
715 }
716
717 wallet.inner.exit.start_exit_for_vtxos(&vtxos).await?;
718
719 let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
720 let effective = -send.total_amount().to_signed()? + exited.to_signed()?;
721 if effective != SignedAmount::ZERO {
722 warn!(
723 "Movement {} should have fee of zero, but got {}: total = {}, exited = {}",
724 htlcs.movement_id, effective, send.total_amount(), exited,
725 );
726 }
727 wallet.inner.movements.finish_movement_with_update(
728 htlcs.movement_id,
729 MovementStatus::Failed,
730 MovementUpdate::new()
731 .effective_balance(effective)
732 .fee(effective.unsigned_abs())
733 .exited_vtxos(&vtxos),
734 ).await?;
735
736 Ok(())
737}
738
739pub(crate) async fn handle_lightning_send_htlcs_revocation(
744 wallet: &Wallet,
745 send: &LightningSend,
746 htlcs: &Htlcs,
747 revocation: &Revocation,
748) -> Result<(), AdvanceError> {
749 let payment_hash = send.invoice.payment_hash();
750 let tip = wallet.inner.chain.tip().await?;
751
752 debug!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
753 payment_hash, tip, send.htlc_expiry);
754
755
756 revoke_lightning_send_htlcs(wallet, send, htlcs, revocation).await
757 .inspect_err(|e| {
758 warn!("Failed to revoke HTLC VTXOs for payment {}: {:#}", payment_hash, e);
759 })
760}