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 pub progress: Progress,
75 pub allow_exit_of_htlcs: bool,
77}
78
79impl LightningSend {
80 pub fn id(&self) -> WalletActionId {
81 ln_pay_action_id(self.invoice.payment_hash())
82 }
83
84 pub fn total_amount(&self) -> Amount {
85 self.payment_amount + self.fee
86 }
87
88 pub async fn is_htlc_near_expiry(&self, wallet: &Wallet) -> anyhow::Result<bool> {
91 let tip = wallet.inner.chain.tip().await?;
92 Ok(tip > self.htlc_expiry
93 .saturating_sub(wallet.config().vtxo_refresh_expiry_threshold))
94 }
95
96 pub fn has_failed_revocation(&self) -> bool {
100 matches!(self.progress, Progress::RevocationStuck { .. })
101 }
102}
103
104#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
105#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
106impl WalletAction for LightningSend {
107 fn id(&self) -> WalletActionId { LightningSend::id(self) }
108
109 async fn advance(self, wallet: &Wallet) -> Result<Advance<Self>, AdvanceError> {
110 let new_progress = match self.progress.clone() {
111 Progress::Start => {
112 let htlcs = request_lightning_send_htlcs(wallet, &self).await?;
113 Progress::HtlcReceived(htlcs)
114 },
115 Progress::HtlcReceived(htlcs) => {
116 initiate_lightning_send_payment(wallet, &self, &htlcs).await?;
117 Progress::PaymentInitiated(htlcs)
118 },
119 Progress::PaymentInitiated(htlcs) => {
120 let wait = false;
121 match check_lightning_send_payment_status(
122 wallet, &self, &htlcs, wait,
123 ).await? {
124 PaymentStatus::Success(preimage) => {
125 settle_lightning_send_payment(wallet, &self, &htlcs, preimage).await?;
126 return Ok(Advance::Done);
127 },
128 PaymentStatus::Failed => {
129 let revocation = fail_lightning_send_payment(wallet, &self).await?;
130 Progress::RevocableHtlcs { htlcs, revocation }
131 },
132 PaymentStatus::Pending => {
133 if self.is_htlc_near_expiry(wallet).await? {
134 let revocation = fail_lightning_send_payment(wallet, &self).await?;
135 Progress::RevocableHtlcs { htlcs, revocation }
136 } else {
137 return Ok(Advance::Park {
138 state: LightningSend {
139 progress: Progress::PaymentInitiated(htlcs),
140 ..self
141 },
142 wake_after: Some(PAYMENT_PENDING_POLL_INTERVAL),
143 error: None,
144 });
145 }
146 },
147 }
148 },
149 Progress::RevocableHtlcs { htlcs, revocation } |
150 Progress::RevocationStuck { htlcs, revocation } => {
151 handle_lightning_send_htlcs_revocation(wallet, &self, &htlcs, &revocation).await?;
152 return Ok(Advance::Done);
153 },
154 };
155
156 Ok(Advance::Next(LightningSend { progress: new_progress, ..self }))
157 }
158
159 async fn on_retry(self, wallet: &Wallet, retries: u32) -> anyhow::Result<Advance<Self>> {
160 match self.progress.clone() {
161 Progress::Start => {
162 if self.is_htlc_near_expiry(wallet).await? {
163 let err = anyhow!("Could not start lightning send and HTLCs are near expiry");
164 return Ok(Advance::Failed(err));
165 }
166 },
167 Progress::HtlcReceived(htlcs) |
168 Progress::PaymentInitiated(htlcs) => {
169 if self.is_htlc_near_expiry(wallet).await? {
170 let revocation = fail_lightning_send_payment(wallet, &self).await?;
171 let next = LightningSend {
172 progress: Progress::RevocableHtlcs { htlcs, revocation },
173 ..self
174 };
175 return Ok(Advance::Next(next));
176 }
177 },
178 Progress::RevocableHtlcs { htlcs, revocation } => {
179 warn!("We could not revoke HTLCs, will continue retrying but the attempt will be marked as such");
180 let next = LightningSend {
181 progress: Progress::RevocationStuck { htlcs, revocation },
182 ..self
183 };
184 return Ok(Advance::Next(next));
185 },
186 Progress::RevocationStuck { htlcs, .. } => {
187 if self.allow_exit_of_htlcs && self.is_htlc_near_expiry(wallet).await? {
188 exit_lightning_send_htlcs(wallet, &self, &htlcs).await?;
189 return Ok(Advance::Done);
190 }
191 },
193 }
194
195 Ok(park_with_backoff(self, retries))
196 }
197
198 async fn on_rejection(self, wallet: &Wallet, error: AdvanceError) -> anyhow::Result<Advance<Self>> {
199 match self.progress.clone() {
200 Progress::Start => {
205 let id = self.id();
206 error!("Could not start lightning send {}: {:?}", id, error);
207 if let Err(cancel_err) = wallet.stop_wallet_action(&id).await {
208 warn!("could not cancel start-phase lightning send {}: {:#}", id, cancel_err);
209 }
210 Ok(Advance::Failed(error.into()))
211 },
212 Progress::HtlcReceived(htlcs) |
213 Progress::PaymentInitiated(htlcs) => {
214 let revocation = fail_lightning_send_payment(wallet, &self).await?;
215 let next = LightningSend {
216 progress: Progress::RevocableHtlcs { htlcs, revocation },
217 ..self
218 };
219 Ok(Advance::Next(next))
220 },
221 Progress::RevocableHtlcs { htlcs, revocation } => {
222 warn!("We could not revoke HTLCs, will continue retrying but the attempt will be marked as such");
223 let next = LightningSend {
224 progress: Progress::RevocationStuck { htlcs, revocation },
225 ..self
226 };
227 Ok(Advance::Next(next))
228 },
229 Progress::RevocationStuck { htlcs, .. } => {
230 if self.allow_exit_of_htlcs && self.is_htlc_near_expiry(wallet).await? {
231 exit_lightning_send_htlcs(wallet, &self, &htlcs).await?;
232 return Ok(Advance::Failed(anyhow!("Server refused to revoke HTLCs, exiting")));
233 }
234 Ok(Advance::Park { state: self, wake_after: None, error: Some(error) })
238 },
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub enum Progress {
248 Start,
250 HtlcReceived(Htlcs),
252 PaymentInitiated(Htlcs),
254 RevocableHtlcs { htlcs: Htlcs, revocation: Revocation },
256 RevocationStuck { htlcs: Htlcs, revocation: Revocation },
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct Htlcs {
267 pub vtxo_ids: Vec<VtxoId>,
268 #[serde(with = "ark::encode::serde")]
269 pub mailbox_id: MailboxIdentifier,
270 pub movement_id: MovementId,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct Revocation {
278 pub key: PublicKey,
279}
280
281const PAYMENT_PENDING_POLL_INTERVAL: Duration = Duration::from_secs(2);
283
284pub(crate) async fn start_lightning_send(
291 wallet: &Wallet,
292 invoice: Invoice,
293 user_amount: Option<Amount>,
294 original_payment_method: PaymentMethod,
295) -> anyhow::Result<LightningSend> {
296 let (_, ark_info) = wallet.require_server().await?;
297 let tip = wallet.inner.chain.tip().await?;
298
299 let properties = wallet.inner.db.read_properties().await?.context("Missing config")?;
300 if invoice.network() != properties.network {
301 bail!("Invoice is for wrong network: {}", invoice.network());
302 }
303
304 invoice.check_signature()?;
305
306 let payment_amount = invoice.get_payment_amount(user_amount)?;
307 if payment_amount == Amount::ZERO {
308 bail!("Cannot pay invoice for 0 sats (0 sat invoices are not any-amount invoices)");
309 }
310
311 let (inputs, fee) = wallet.select_vtxos_to_cover_with_fee(
312 payment_amount,
313 |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
314 ).await.context("Could not find enough suitable VTXOs to cover lightning payment")?;
315
316 let action_id = ln_pay_action_id(invoice.payment_hash());
317 wallet.lock_vtxos(
318 &inputs,
319 Some(crate::vtxo::VtxoLockHolder::Action { id: action_id }),
320 ).await?;
321
322 let (change_keypair, _) = wallet.derive_store_next_keypair().await?;
323
324 let htlc_expiry = tip + ark_info.htlc_send_expiry_delta as BlockHeight;
325
326 Ok(LightningSend {
327 invoice,
328 original_payment_method,
329 input_vtxo_ids: inputs.iter().map(|v| v.id()).collect(),
330 payment_amount,
331 fee,
332 htlc_key: change_keypair.public_key(),
333 htlc_expiry,
334 allow_exit_of_htlcs: false,
335 progress: Progress::Start,
336 })
337}
338
339pub(crate) async fn request_lightning_send_htlcs(
347 wallet: &Wallet,
348 send: &LightningSend,
349) -> Result<Htlcs, AdvanceError> {
350 let (mut srv, _) = wallet.require_server().await?;
351
352 let full_inputs = wallet.inner.db.get_full_vtxos(&send.input_vtxo_ids).await
353 .context("failed to hydrate lightning-send input vtxos")?;
354
355 wallet.register_vtxo_transactions_with_server(&full_inputs).await
357 .context("failed to register lightning-send input vtxo transactions with server")?;
358
359 let mut input_keypairs = Vec::with_capacity(full_inputs.len());
360 for input in full_inputs.iter() {
361 input_keypairs.push(wallet.get_vtxo_key(input).await?);
362 }
363
364 let policy = VtxoPolicy::new_server_htlc_send(
365 send.htlc_key, send.invoice.payment_hash(), send.htlc_expiry,
366 );
367 let total_amount = send.total_amount();
368 let input_amount = full_inputs.iter().map(|v| v.amount()).sum::<Amount>();
369 let pay_dest = ArkoorDestination { total_amount, policy };
370 let outputs = if input_amount == total_amount {
371 vec![pay_dest]
372 } else {
373 let change_dest = ArkoorDestination {
374 total_amount: input_amount - total_amount,
375 policy: VtxoPolicy::new_pubkey(send.htlc_key),
376 };
377 vec![pay_dest, change_dest]
378 };
379
380 let builder = ArkoorPackageBuilder::new_with_checkpoints(
381 full_inputs.clone(),
382 outputs,
383 )
384 .context("Failed to construct arkoor package")?
385 .generate_user_nonces(&input_keypairs)
386 .context("invalid nb of keypairs")?;
387
388 let cosign_request = protos::LightningPayHtlcCosignRequest {
389 parts: protos::ArkoorPackageCosignRequest::from(builder.cosign_request()).parts,
390 };
391 let response = srv.client.request_lightning_pay_htlc_cosign(cosign_request).await
392 .map_err(AdvanceError::Server)?.into_inner();
393 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
394 .context("Failed to parse cosign response from server")?;
395
396 let vtxos = builder
397 .user_cosign(&input_keypairs, cosign_responses)
398 .context("Failed to cosign vtxos")?
399 .build_signed_vtxos();
400
401 let (htlc_vtxos, change_vtxos) = vtxos.clone().into_iter()
402 .partition::<Vec<_>, _>(|v| matches!(v.policy(), VtxoPolicy::ServerHtlcSend(_)));
403
404 let mut effective_balance = Amount::ZERO;
405 for vtxo in &htlc_vtxos {
406 wallet.validate_vtxo(vtxo).await?;
407 effective_balance += vtxo.amount();
408 }
409 for change in &change_vtxos {
410 let last_input = full_inputs.last().context("no inputs provided")?;
411 let tx = wallet.inner.chain.get_tx(&last_input.chain_anchor().txid).await?;
412 let tx = tx.with_context(|| format!(
413 "input vtxo chain anchor not found for lightning change vtxo: {}",
414 last_input.chain_anchor().txid,
415 ))?;
416 change.validate(&tx).context("invalid lightning change vtxo")?;
417 }
418
419 if let Err(e) = wallet.register_vtxo_transactions_with_server(&vtxos).await {
420 warn!("failed to register lightning-send output vtxo transactions with server: {:#}", e);
421 }
422
423 let movement_id = wallet.inner.movements.new_movement_with_update(
424 Subsystem::LIGHTNING_SEND,
425 LightningSendMovement::Send.to_string(),
426 MovementUpdate::new()
427 .intended_balance(-send.payment_amount.to_signed().context("payment amount out of range")?)
428 .effective_balance(-effective_balance.to_signed().context("effective balance out of range")?)
429 .fee(send.fee)
430 .consumed_vtxos(&full_inputs)
431 .sent_to([MovementDestination::new(send.original_payment_method.clone(), send.payment_amount)])
432 .metadata(LightningMovement::metadata(send.invoice.payment_hash(), &htlc_vtxos, None))
433 ).await.context("failed to create movement")?;
434 wallet.store_locked_vtxos(
435 &htlc_vtxos,
436 Some(VtxoLockHolder::Movement { id: movement_id })
437 ).await?;
438 wallet.mark_vtxos_as_spent(&send.input_vtxo_ids).await?;
439 wallet.store_spendable_vtxos(&change_vtxos).await?;
440 wallet.inner.movements.update_movement(
441 movement_id,
442 MovementUpdate::new()
443 .produced_vtxos(change_vtxos)
444 .metadata(LightningMovement::metadata(send.invoice.payment_hash(), &htlc_vtxos, None))
445 ).await.context("failed to update movement")?;
446
447 Ok(Htlcs {
448 vtxo_ids: htlc_vtxos.iter().map(|v| v.id()).collect(),
449 mailbox_id: wallet.mailbox_identifier(),
450 movement_id,
451 })
452}
453
454pub(crate) async fn initiate_lightning_send_payment(
458 wallet: &Wallet,
459 send: &LightningSend,
460 htlcs: &Htlcs,
461) -> Result<(), AdvanceError> {
462 let (mut srv, _) = wallet.require_server().await?;
463
464 let req = protos::InitiateLightningPaymentRequest {
465 invoice: send.invoice.to_string(),
466 htlc_vtxo_ids: htlcs.vtxo_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
467 payment_amount_sat: send.payment_amount.to_sat(),
468 mailbox_id: Some(htlcs.mailbox_id.serialize()),
469 };
470 srv.client.initiate_lightning_payment(req).await
471 .map_err(AdvanceError::Server)?;
472
473 Ok(())
474}
475
476pub(crate) async fn check_lightning_send_payment_status(
480 wallet: &Wallet,
481 send: &LightningSend,
482 htlcs: &Htlcs,
483 wait: bool,
484) -> anyhow::Result<PaymentStatus> {
485 let (mut srv, _) = wallet.require_server().await?;
486 let payment_hash = send.invoice.payment_hash();
487
488 let mut htlc_vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
489 for id in htlcs.vtxo_ids.iter() {
490 htlc_vtxos.push(wallet.get_vtxo_by_id(*id).await?);
491 }
492
493 let policy = htlc_vtxos.iter()
494 .all_same(|v| v.vtxo.policy())
495 .context("All lightning htlc should have the same policy")?;
496 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
497 if policy.payment_hash != payment_hash {
498 bail!("Payment hash mismatch on stored HTLC policy");
499 }
500
501 let tip = wallet.inner.chain.tip().await?;
502 let expired = tip > policy.htlc_expiry;
503 let pending_status = if expired { PaymentStatus::Failed } else { PaymentStatus::Pending };
504
505 let req = protos::CheckLightningPaymentRequest {
506 hash: payment_hash.to_vec(),
507 wait,
508 };
509 let response = srv.client.check_lightning_payment(req).await
512 .map(|r| r.into_inner().payment_status);
513
514 match response {
515 Ok(Some(lightning_payment_status::PaymentStatus::Success(s))) => {
516 match Preimage::try_from(s.preimage) {
517 Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
518 Ok(PaymentStatus::Success(preimage))
519 },
520 other => {
521 error!(
522 "Server reported success but returned an invalid preimage for {}: {:?}",
523 payment_hash, other,
524 );
525 Ok(pending_status)
526 },
527 }
528 },
529 Ok(Some(lightning_payment_status::PaymentStatus::Failed(_))) => {
530 Ok(PaymentStatus::Failed)
531 },
532 Ok(Some(lightning_payment_status::PaymentStatus::Pending(_))) => {
533 trace!("Payment {} is still pending", payment_hash);
534 Ok(pending_status)
535 },
536 Ok(None) | Err(_) => Ok(pending_status),
537 }
538}
539
540pub(crate) async fn settle_lightning_send_payment(
543 wallet: &Wallet,
544 send: &LightningSend,
545 htlcs: &Htlcs,
546 preimage: Preimage,
547) -> anyhow::Result<()> {
548 let payment_hash = send.invoice.payment_hash();
549 if preimage.compute_payment_hash() != payment_hash {
550 bail!("preimage does not match payment hash {}", payment_hash);
551 }
552 info!(
553 "Lightning payment succeeded! Preimage: {}. Payment hash: {}",
554 preimage.as_hex(), payment_hash.as_hex(),
555 );
556
557 wallet.inner.db.record_paid_invoice(payment_hash, preimage).await?;
558 wallet.mark_vtxos_as_spent(&htlcs.vtxo_ids).await?;
559 wallet.inner.movements.finish_movement_with_update(
560 htlcs.movement_id,
561 MovementStatus::Successful,
562 MovementUpdate::new().metadata([(
563 "payment_preimage".into(),
564 serde_json::to_value(preimage).expect("payment preimage can serde"),
565 )]),
566 ).await?;
567
568 Ok(())
569}
570
571pub(crate) async fn fail_lightning_send_payment(
575 wallet: &Wallet,
576 send: &LightningSend,
577) -> anyhow::Result<Revocation> {
578 info!("Lightning payment {} failed, preparing to revoke", send.invoice.payment_hash());
579 let (revocation_keypair, _) = wallet.derive_store_next_keypair().await?;
580 Ok(Revocation { key: revocation_keypair.public_key() })
581}
582
583pub(crate) async fn revoke_lightning_send_htlcs(
587 wallet: &Wallet,
588 send: &LightningSend,
589 htlcs: &Htlcs,
590 revocation: &Revocation,
591) -> Result<(), AdvanceError> {
592 let (mut srv, _) = wallet.require_server().await?;
593
594 debug!("Revoking {} HTLC vtxos for payment {}",
595 htlcs.vtxo_ids.len(), send.invoice.payment_hash());
596
597 let mut htlc_keypairs = Vec::with_capacity(htlcs.vtxo_ids.len());
598 let mut htlc_vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
599 for id in htlcs.vtxo_ids.iter() {
600 let vtxo = wallet.inner.db.get_full_vtxo(*id).await?
601 .with_context(|| format!("htlc vtxo with id {} not found", id))?;
602 htlc_keypairs.push(wallet.get_vtxo_key(&vtxo).await?);
603 htlc_vtxos.push(vtxo);
604 }
605
606 let revocation_claim_policy = VtxoPolicy::new_pubkey(revocation.key);
607 let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
608 htlc_vtxos.iter().cloned(),
609 revocation_claim_policy,
610 )
611 .context("Failed to construct arkoor package")?
612 .generate_user_nonces(&htlc_keypairs)
613 .context("failed to generate user nonces")?;
614
615 let cosign_request = protos::ArkoorPackageCosignRequest::from(builder.cosign_request());
616 let response = srv.client
617 .request_lightning_pay_htlc_revocation(cosign_request).await
618 .map_err(AdvanceError::Server)?.into_inner();
619 let cosign_resp = ArkoorPackageCosignResponse::try_from(response)
620 .context("Failed to parse cosign response from server")?;
621
622 let vtxos = builder
623 .user_cosign(&htlc_keypairs, cosign_resp)
624 .context("Failed to cosign vtxos")?
625 .build_signed_vtxos();
626
627 let revoked = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
628 let effective = -send.total_amount().to_signed().context("total amount out of range")? +
629 revoked.to_signed().context("revoked amount out of range")?;
630 if effective != SignedAmount::ZERO {
631 warn!(
632 "Movement {} should have fee of zero, but got {}: total = {}, revoked = {}",
633 htlcs.movement_id, effective, send.total_amount(), revoked,
634 );
635 }
636 wallet.inner.movements.finish_movement_with_update(
637 htlcs.movement_id,
638 MovementStatus::Failed,
639 MovementUpdate::new()
640 .effective_balance(effective)
641 .fee(effective.unsigned_abs())
642 .produced_vtxos(&vtxos),
643 ).await.context("failed to update movement")?;
644 wallet.store_spendable_vtxos(&vtxos).await?;
645 wallet.mark_vtxos_as_spent(&htlc_vtxos).await?;
646
647 Ok(())
648}
649
650pub(crate) async fn exit_lightning_send_htlcs(
654 wallet: &Wallet,
655 send: &LightningSend,
656 htlcs: &Htlcs,
657) -> anyhow::Result<()> {
658 let payment_hash = send.invoice.payment_hash();
659 warn!("HTLC VTXOs for payment {} are near expiry, marking to exit", payment_hash);
660
661 let mut vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
662 for id in htlcs.vtxo_ids.iter() {
663 vtxos.push(wallet.get_vtxo_by_id(*id).await?.vtxo);
664 }
665
666 wallet.inner.exit.start_exit_for_vtxos(&vtxos).await?;
667
668 let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
669 let effective = -send.total_amount().to_signed()? + exited.to_signed()?;
670 if effective != SignedAmount::ZERO {
671 warn!(
672 "Movement {} should have fee of zero, but got {}: total = {}, exited = {}",
673 htlcs.movement_id, effective, send.total_amount(), exited,
674 );
675 }
676 wallet.inner.movements.finish_movement_with_update(
677 htlcs.movement_id,
678 MovementStatus::Failed,
679 MovementUpdate::new()
680 .effective_balance(effective)
681 .fee(effective.unsigned_abs())
682 .exited_vtxos(&vtxos),
683 ).await?;
684
685 Ok(())
686}
687
688pub(crate) async fn handle_lightning_send_htlcs_revocation(
693 wallet: &Wallet,
694 send: &LightningSend,
695 htlcs: &Htlcs,
696 revocation: &Revocation,
697) -> Result<(), AdvanceError> {
698 let payment_hash = send.invoice.payment_hash();
699 let tip = wallet.inner.chain.tip().await?;
700
701 debug!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
702 payment_hash, tip, send.htlc_expiry);
703
704
705 revoke_lightning_send_htlcs(wallet, send, htlcs, revocation).await
706 .inspect_err(|e| {
707 warn!("Failed to revoke HTLC VTXOs for payment {}: {:#}", payment_hash, e);
708 })
709}