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