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) -> anyhow::Result<Advance<Self>> {
171 match self.progress.clone() {
172 Progress::Start => {
173 if self.is_htlc_near_expiry(wallet).await? {
174 let err = anyhow!("Could not start lightning send and HTLCs are near expiry");
175 return Ok(Advance::Failed(err));
176 }
177 },
178 Progress::HtlcReceived(htlcs) |
179 Progress::PaymentInitiated(htlcs) => {
180 if self.is_htlc_near_expiry(wallet).await? {
181 let revocation = fail_lightning_send_payment(wallet, &self).await?;
182 let next = LightningSend {
183 progress: Progress::RevocableHtlcs { htlcs, revocation },
184 ..self
185 };
186 return Ok(Advance::Next(next));
187 }
188 },
189 Progress::RevocableHtlcs { htlcs, revocation } => {
190 warn!("We could not revoke HTLCs, will continue retrying but the attempt will be marked as such");
191 let next = LightningSend {
192 progress: Progress::RevocationStuck { htlcs, revocation },
193 ..self
194 };
195 return Ok(Advance::Next(next));
196 },
197 Progress::RevocationStuck { htlcs, .. } => {
198 if self.allow_exit_of_htlcs && self.is_htlc_near_expiry(wallet).await? {
199 exit_lightning_send_htlcs(wallet, &self, &htlcs).await?;
200 return Ok(Advance::Done);
201 }
202 },
204 }
205
206 Ok(park_with_backoff(self, retries))
207 }
208
209 async fn on_rejection(self, wallet: &Wallet, error: AdvanceError) -> anyhow::Result<Advance<Self>> {
210 match self.progress.clone() {
211 Progress::Start => {
216 let id = self.id();
217 error!("Could not start lightning send {}: {:?}", id, error);
218 if let Err(cancel_err) = wallet.stop_wallet_action(&id).await {
219 warn!("could not cancel start-phase lightning send {}: {:#}", id, cancel_err);
220 }
221 Ok(Advance::Failed(error.into()))
222 },
223 Progress::HtlcReceived(htlcs) |
224 Progress::PaymentInitiated(htlcs) => {
225 let revocation = fail_lightning_send_payment(wallet, &self).await?;
226 let next = LightningSend {
227 progress: Progress::RevocableHtlcs { htlcs, revocation },
228 ..self
229 };
230 Ok(Advance::Next(next))
231 },
232 Progress::RevocableHtlcs { htlcs, revocation } => {
233 warn!("We could not revoke HTLCs, will continue retrying but the attempt will be marked as such");
234 let next = LightningSend {
235 progress: Progress::RevocationStuck { htlcs, revocation },
236 ..self
237 };
238 Ok(Advance::Next(next))
239 },
240 Progress::RevocationStuck { htlcs, .. } => {
241 if self.allow_exit_of_htlcs && self.is_htlc_near_expiry(wallet).await? {
242 exit_lightning_send_htlcs(wallet, &self, &htlcs).await?;
243 return Ok(Advance::Failed(anyhow!("Server refused to revoke HTLCs, exiting")));
244 }
245 Ok(Advance::Park { state: self, wake_after: None, error: Some(error) })
249 },
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub enum Progress {
259 Start,
261 HtlcReceived(Htlcs),
263 PaymentInitiated(Htlcs),
265 RevocableHtlcs { htlcs: Htlcs, revocation: Revocation },
267 RevocationStuck { htlcs: Htlcs, revocation: Revocation },
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct Htlcs {
278 pub vtxo_ids: Vec<VtxoId>,
279 #[serde(with = "ark::encode::serde")]
280 pub mailbox_id: MailboxIdentifier,
281 pub movement_id: MovementId,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288pub struct Revocation {
289 pub key: PublicKey,
290}
291
292const PAYMENT_PENDING_POLL_INTERVAL: Duration = Duration::from_secs(2);
294
295pub(crate) async fn start_lightning_send(
302 wallet: &Wallet,
303 invoice: Invoice,
304 user_amount: Option<Amount>,
305 original_payment_method: PaymentMethod,
306) -> anyhow::Result<LightningSend> {
307 let (_, ark_info) = wallet.require_server().await?;
308 let tip = wallet.inner.chain.tip().await?;
309
310 let properties = wallet.inner.db.read_properties().await?.context("Missing config")?;
311 if invoice.network() != properties.network {
312 bail!("Invoice is for wrong network: {}", invoice.network());
313 }
314
315 invoice.check_signature()?;
316
317 let payment_amount = invoice.get_payment_amount(user_amount)?;
318 if payment_amount == Amount::ZERO {
319 bail!("Cannot pay invoice for 0 sats (0 sat invoices are not any-amount invoices)");
320 }
321
322 let (inputs, fee) = wallet.select_vtxos_to_cover_with_fee(
323 payment_amount,
324 |a, v| ark_info.fees.lightning_send.calculate(a, v).context("fee overflowed"),
325 ).await.context("Could not find enough suitable VTXOs to cover lightning payment")?;
326
327 let action_id = ln_pay_action_id(invoice.payment_hash());
328 wallet.lock_vtxos(
329 &inputs,
330 Some(crate::vtxo::VtxoLockHolder::Action { id: action_id }),
331 ).await?;
332
333 let (change_keypair, _) = wallet.derive_store_next_keypair().await?;
334 let (revocation_keypair, _) = wallet.derive_store_next_keypair().await?;
335
336 let htlc_expiry = tip + ark_info.htlc_send_expiry_delta as BlockHeight;
337
338 let movement_id = wallet.inner.movements.new_movement_with_update(
339 Subsystem::LIGHTNING_SEND,
340 LightningSendMovement::Send.to_string(),
341 MovementUpdate::new()
342 .intended_balance(-payment_amount.to_signed().context("payment amount out of range")?)
343 .fee(fee)
344 .consumed_vtxos(&inputs)
345 .sent_to([MovementDestination::new(original_payment_method.clone(), payment_amount)])
346 .metadata(LightningMovement::metadata(invoice.payment_hash(), Vec::<VtxoId>::new(), None))
347 ).await.context("failed to create lightning-send movement")?;
348
349 Ok(LightningSend {
350 invoice,
351 original_payment_method,
352 input_vtxo_ids: inputs.iter().map(|v| v.id()).collect(),
353 payment_amount,
354 fee,
355 htlc_key: change_keypair.public_key(),
356 htlc_expiry,
357 movement_id: Some(movement_id),
358 revocation_key: Some(revocation_keypair.public_key()),
359 allow_exit_of_htlcs: false,
360 progress: Progress::Start,
361 })
362}
363
364pub(crate) async fn request_lightning_send_htlcs(
372 wallet: &Wallet,
373 send: &LightningSend,
374) -> Result<Htlcs, AdvanceError> {
375 let (mut srv, _) = wallet.require_server().await?;
376
377 let full_inputs = wallet.inner.db.get_full_vtxos(&send.input_vtxo_ids).await
378 .context("failed to hydrate lightning-send input vtxos")?;
379
380 wallet.register_vtxo_transactions_with_server(&full_inputs).await
382 .context("failed to register lightning-send input vtxo transactions with server")?;
383
384 let mut input_keypairs = Vec::with_capacity(full_inputs.len());
385 for input in full_inputs.iter() {
386 input_keypairs.push(wallet.get_vtxo_key(input).await?);
387 }
388
389 let policy = VtxoPolicy::new_server_htlc_send(
390 send.htlc_key, send.invoice.payment_hash(), send.htlc_expiry,
391 );
392 let total_amount = send.total_amount();
393 let input_amount = full_inputs.iter().map(|v| v.amount()).sum::<Amount>();
394 let pay_dest = ArkoorDestination { total_amount, policy };
395 let outputs = if input_amount == total_amount {
396 vec![pay_dest]
397 } else {
398 let change_dest = ArkoorDestination {
399 total_amount: input_amount - total_amount,
400 policy: VtxoPolicy::new_pubkey(send.htlc_key),
401 };
402 vec![pay_dest, change_dest]
403 };
404
405 let builder = ArkoorPackageBuilder::new_with_checkpoints(
406 full_inputs.clone(),
407 outputs,
408 )
409 .context("Failed to construct arkoor package")?
410 .generate_user_nonces(&input_keypairs)
411 .context("invalid nb of keypairs")?;
412
413 let cosign_request = protos::LightningPayHtlcCosignRequest {
414 parts: protos::ArkoorPackageCosignRequest::from(builder.cosign_request()).parts,
415 };
416 let response = srv.client.request_lightning_pay_htlc_cosign(cosign_request).await
417 .map_err(AdvanceError::Server)?.into_inner();
418 let cosign_responses = ArkoorPackageCosignResponse::try_from(response)
419 .context("Failed to parse cosign response from server")?;
420
421 let vtxos = builder
422 .user_cosign(&input_keypairs, cosign_responses)
423 .context("Failed to cosign vtxos")?
424 .build_signed_vtxos();
425
426 let (htlc_vtxos, change_vtxos) = vtxos.clone().into_iter()
427 .partition::<Vec<_>, _>(|v| matches!(v.policy(), VtxoPolicy::ServerHtlcSend(_)));
428
429 let mut effective_balance = Amount::ZERO;
430 for vtxo in &htlc_vtxos {
431 wallet.validate_vtxo(vtxo).await?;
432 effective_balance += vtxo.amount();
433 }
434 for change in &change_vtxos {
435 let last_input = full_inputs.last().context("no inputs provided")?;
436 let tx = wallet.inner.chain.get_tx(&last_input.chain_anchor().txid).await?;
437 let tx = tx.with_context(|| format!(
438 "input vtxo chain anchor not found for lightning change vtxo: {}",
439 last_input.chain_anchor().txid,
440 ))?;
441 change.validate(&tx).context("invalid lightning change vtxo")?;
442 }
443
444 if let Err(e) = wallet.register_vtxo_transactions_with_server(&vtxos).await {
445 warn!("failed to register lightning-send output vtxo transactions with server: {:#}", e);
446 }
447
448 let movement_id = match send.movement_id {
455 Some(id) => id,
456 None => wallet.inner.movements.new_movement_with_update(
457 Subsystem::LIGHTNING_SEND,
458 LightningSendMovement::Send.to_string(),
459 MovementUpdate::new()
460 .intended_balance(-send.payment_amount.to_signed().context("payment amount out of range")?)
461 .fee(send.fee)
462 .consumed_vtxos(&full_inputs)
463 .sent_to([MovementDestination::new(send.original_payment_method.clone(), send.payment_amount)])
464 ).await.context("failed to create lightning-send movement")?,
465 };
466 wallet.store_locked_vtxos(
467 &htlc_vtxos,
468 Some(VtxoLockHolder::Movement { id: movement_id })
469 ).await?;
470 wallet.mark_vtxos_as_spent(&send.input_vtxo_ids).await?;
471 wallet.store_spendable_vtxos(&change_vtxos).await?;
472 wallet.inner.movements.update_movement(
473 movement_id,
474 MovementUpdate::new()
475 .effective_balance(-effective_balance.to_signed().context("effective balance out of range")?)
476 .produced_vtxos(change_vtxos)
477 .metadata(LightningMovement::metadata(send.invoice.payment_hash(), &htlc_vtxos, None))
478 ).await.context("failed to update lightning-send movement")?;
479
480 Ok(Htlcs {
481 vtxo_ids: htlc_vtxos.iter().map(|v| v.id()).collect(),
482 mailbox_id: wallet.mailbox_identifier(),
483 movement_id,
484 })
485}
486
487pub(crate) async fn initiate_lightning_send_payment(
491 wallet: &Wallet,
492 send: &LightningSend,
493 htlcs: &Htlcs,
494) -> Result<(), AdvanceError> {
495 let (mut srv, _) = wallet.require_server().await?;
496
497 let req = protos::InitiateLightningPaymentRequest {
498 invoice: send.invoice.to_string(),
499 htlc_vtxo_ids: htlcs.vtxo_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
500 payment_amount_sat: send.payment_amount.to_sat(),
501 mailbox_id: Some(htlcs.mailbox_id.serialize()),
502 };
503 srv.client.initiate_lightning_payment(req).await
504 .map_err(AdvanceError::Server)?;
505
506 Ok(())
507}
508
509pub(crate) async fn check_lightning_send_payment_status(
513 wallet: &Wallet,
514 send: &LightningSend,
515 htlcs: &Htlcs,
516 wait: bool,
517) -> anyhow::Result<PaymentStatus> {
518 let (mut srv, _) = wallet.require_server().await?;
519 let payment_hash = send.invoice.payment_hash();
520
521 let mut htlc_vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
522 for id in htlcs.vtxo_ids.iter() {
523 htlc_vtxos.push(wallet.get_vtxo_by_id(*id).await?);
524 }
525
526 let policy = htlc_vtxos.iter()
527 .all_same(|v| v.vtxo.policy())
528 .context("All lightning htlc should have the same policy")?;
529 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
530 if policy.payment_hash != payment_hash {
531 bail!("Payment hash mismatch on stored HTLC policy");
532 }
533
534 let tip = wallet.inner.chain.tip().await?;
535 let expired = tip > policy.htlc_expiry;
536 let pending_status = if expired { PaymentStatus::Failed } else { PaymentStatus::Pending };
537
538 let req = protos::CheckLightningPaymentRequest {
539 hash: payment_hash.to_vec(),
540 wait,
541 };
542 let response = srv.client.check_lightning_payment(req).await
545 .map(|r| r.into_inner().payment_status);
546
547 match response {
548 Ok(Some(lightning_payment_status::PaymentStatus::Success(s))) => {
549 match Preimage::try_from(s.preimage) {
550 Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
551 Ok(PaymentStatus::Success(preimage))
552 },
553 other => {
554 error!(
555 "Server reported success but returned an invalid preimage for {}: {:?}",
556 payment_hash, other,
557 );
558 Ok(pending_status)
559 },
560 }
561 },
562 Ok(Some(lightning_payment_status::PaymentStatus::Failed(_))) => {
563 Ok(PaymentStatus::Failed)
564 },
565 Ok(Some(lightning_payment_status::PaymentStatus::Pending(_))) => {
566 trace!("Payment {} is still pending", payment_hash);
567 Ok(pending_status)
568 },
569 Ok(None) | Err(_) => Ok(pending_status),
570 }
571}
572
573pub(crate) async fn settle_lightning_send_payment(
576 wallet: &Wallet,
577 send: &LightningSend,
578 htlcs: &Htlcs,
579 preimage: Preimage,
580) -> anyhow::Result<()> {
581 let payment_hash = send.invoice.payment_hash();
582 if preimage.compute_payment_hash() != payment_hash {
583 bail!("preimage does not match payment hash {}", payment_hash);
584 }
585 info!(
586 "Lightning payment succeeded! Preimage: {}. Payment hash: {}",
587 preimage.as_hex(), payment_hash.as_hex(),
588 );
589
590 wallet.inner.db.record_paid_invoice(payment_hash, preimage).await?;
591 wallet.mark_vtxos_as_spent(&htlcs.vtxo_ids).await?;
592 wallet.inner.movements.finish_movement_with_update(
593 htlcs.movement_id,
594 MovementStatus::Successful,
595 MovementUpdate::new().metadata([(
596 "payment_preimage".into(),
597 serde_json::to_value(preimage).expect("payment preimage can serde"),
598 )]),
599 ).await?;
600
601 Ok(())
602}
603
604pub(crate) async fn fail_lightning_send_payment(
608 wallet: &Wallet,
609 send: &LightningSend,
610) -> anyhow::Result<Revocation> {
611 info!("Lightning payment {} failed, preparing to revoke", send.invoice.payment_hash());
612 let key = match send.revocation_key {
617 Some(key) => key,
618 None => wallet.derive_store_next_keypair().await?.0.public_key(),
619 };
620 Ok(Revocation { key })
621}
622
623pub(crate) async fn revoke_lightning_send_htlcs(
627 wallet: &Wallet,
628 send: &LightningSend,
629 htlcs: &Htlcs,
630 revocation: &Revocation,
631) -> Result<(), AdvanceError> {
632 let (mut srv, _) = wallet.require_server().await?;
633
634 debug!("Revoking {} HTLC vtxos for payment {}",
635 htlcs.vtxo_ids.len(), send.invoice.payment_hash());
636
637 let mut htlc_keypairs = Vec::with_capacity(htlcs.vtxo_ids.len());
638 let mut htlc_vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
639 for id in htlcs.vtxo_ids.iter() {
640 let vtxo = wallet.inner.db.get_full_vtxo(*id).await?
641 .with_context(|| format!("htlc vtxo with id {} not found", id))?;
642 htlc_keypairs.push(wallet.get_vtxo_key(&vtxo).await?);
643 htlc_vtxos.push(vtxo);
644 }
645
646 let revocation_claim_policy = VtxoPolicy::new_pubkey(revocation.key);
647 let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
648 htlc_vtxos.iter().cloned(),
649 revocation_claim_policy,
650 )
651 .context("Failed to construct arkoor package")?
652 .generate_user_nonces(&htlc_keypairs)
653 .context("failed to generate user nonces")?;
654
655 let cosign_request = protos::ArkoorPackageCosignRequest::from(builder.cosign_request());
656 let response = srv.client
657 .request_lightning_pay_htlc_revocation(cosign_request).await
658 .map_err(AdvanceError::Server)?.into_inner();
659 let cosign_resp = ArkoorPackageCosignResponse::try_from(response)
660 .context("Failed to parse cosign response from server")?;
661
662 let vtxos = builder
663 .user_cosign(&htlc_keypairs, cosign_resp)
664 .context("Failed to cosign vtxos")?
665 .build_signed_vtxos();
666
667 let revoked = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
668 let effective = -send.total_amount().to_signed().context("total amount out of range")? +
669 revoked.to_signed().context("revoked amount out of range")?;
670 if effective != SignedAmount::ZERO {
671 warn!(
672 "Movement {} should have fee of zero, but got {}: total = {}, revoked = {}",
673 htlcs.movement_id, effective, send.total_amount(), revoked,
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 .produced_vtxos(&vtxos),
683 ).await.context("failed to update movement")?;
684 wallet.store_spendable_vtxos(&vtxos).await?;
685 wallet.mark_vtxos_as_spent(&htlc_vtxos).await?;
686
687 Ok(())
688}
689
690pub(crate) async fn exit_lightning_send_htlcs(
694 wallet: &Wallet,
695 send: &LightningSend,
696 htlcs: &Htlcs,
697) -> anyhow::Result<()> {
698 let payment_hash = send.invoice.payment_hash();
699 warn!("HTLC VTXOs for payment {} are near expiry, marking to exit", payment_hash);
700
701 let mut vtxos = Vec::with_capacity(htlcs.vtxo_ids.len());
702 for id in htlcs.vtxo_ids.iter() {
703 vtxos.push(wallet.get_vtxo_by_id(*id).await?.vtxo);
704 }
705
706 wallet.inner.exit.start_exit_for_vtxos(&vtxos).await?;
707
708 let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
709 let effective = -send.total_amount().to_signed()? + exited.to_signed()?;
710 if effective != SignedAmount::ZERO {
711 warn!(
712 "Movement {} should have fee of zero, but got {}: total = {}, exited = {}",
713 htlcs.movement_id, effective, send.total_amount(), exited,
714 );
715 }
716 wallet.inner.movements.finish_movement_with_update(
717 htlcs.movement_id,
718 MovementStatus::Failed,
719 MovementUpdate::new()
720 .effective_balance(effective)
721 .fee(effective.unsigned_abs())
722 .exited_vtxos(&vtxos),
723 ).await?;
724
725 Ok(())
726}
727
728pub(crate) async fn handle_lightning_send_htlcs_revocation(
733 wallet: &Wallet,
734 send: &LightningSend,
735 htlcs: &Htlcs,
736 revocation: &Revocation,
737) -> Result<(), AdvanceError> {
738 let payment_hash = send.invoice.payment_hash();
739 let tip = wallet.inner.chain.tip().await?;
740
741 debug!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
742 payment_hash, tip, send.htlc_expiry);
743
744
745 revoke_lightning_send_htlcs(wallet, send, htlcs, revocation).await
746 .inspect_err(|e| {
747 warn!("Failed to revoke HTLC VTXOs for payment {}: {:#}", payment_hash, e);
748 })
749}