1use std::fmt;
2
3use anyhow::Context;
4use bitcoin::Amount;
5use bitcoin::hex::DisplayHex;
6use lightning::util::ser::Writeable;
7use lnurllib::lightning_address::LightningAddress;
8use log::{error, info, trace, warn};
9use server_rpc::protos::{self, lightning_payment_status::PaymentStatus};
10
11use ark::arkoor::ArkoorPackageBuilder;
12use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, PaymentHash, Preimage};
13use ark::{ProtocolEncoding, VtxoPolicy, VtxoRequest, musig};
14use bitcoin_ext::P2TR_DUST;
15
16use crate::Wallet;
17use crate::lightning::lnaddr_invoice;
18use crate::movement::{MovementDestination, MovementStatus, PaymentMethod};
19use crate::movement::update::MovementUpdate;
20use crate::persist::models::LightningSend;
21use crate::subsystem::{LightningMovement, LightningSendMovement, Subsystem};
22
23
24impl Wallet {
25 async fn process_lightning_revocation(&self, payment: &LightningSend) -> anyhow::Result<()> {
46 let mut srv = self.require_server()?;
47 let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
48 .map(|v| v.vtxo).collect::<Vec<_>>();
49
50 info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
51
52 let mut secs = Vec::with_capacity(htlc_vtxos.len());
53 let mut pubs = Vec::with_capacity(htlc_vtxos.len());
54 let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
55 for input in htlc_vtxos.iter() {
56 let keypair = self.get_vtxo_key(input).await?;
57 let (s, p) = musig::nonce_pair(&keypair);
58 secs.push(s);
59 pubs.push(p);
60 keypairs.push(keypair);
61 }
62
63 let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
64
65 let req = protos::RevokeLightningPayHtlcRequest {
66 htlc_vtxo_ids: revocation.arkoors.iter()
67 .map(|i| i.input.id().to_bytes().to_vec())
68 .collect(),
69 user_nonces: revocation.arkoors.iter()
70 .map(|i| i.user_nonce.serialize().to_vec())
71 .collect(),
72 };
73 let cosign_resp: Vec<_> = srv.client.request_lightning_pay_htlc_revocation(req).await?
74 .into_inner().try_into().context("invalid server cosign response")?;
75 ensure!(revocation.verify_cosign_response(&cosign_resp),
76 "invalid arkoor cosignature received from server",
77 );
78
79 let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
80 let mut revoked = Amount::ZERO;
81 for vtxo in &vtxos {
82 info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
83 revoked += vtxo.amount();
84 }
85
86 let count = vtxos.len();
87 self.movements.finish_movement_with_update(
88 payment.movement_id,
89 MovementStatus::Failed,
90 MovementUpdate::new()
91 .effective_balance(-payment.amount.to_signed()? + revoked.to_signed()?)
92 .produced_vtxos(&vtxos)
93 ).await?;
94 self.store_spendable_vtxos(&vtxos).await?;
95 self.mark_vtxos_as_spent(&htlc_vtxos).await?;
96
97 self.db.remove_lightning_send(payment.invoice.payment_hash()).await?;
98
99 info!("Revoked {} HTLC VTXOs", count);
100
101 Ok(())
102 }
103
104 async fn process_lightning_send_server_preimage(
119 &self,
120 preimage: Option<Vec<u8>>,
121 payment: &LightningSend,
122 ) -> anyhow::Result<Option<Preimage>> {
123 let payment_hash = payment.invoice.payment_hash();
124 let preimage_res = preimage
125 .context("preimage is missing")
126 .map(|p| Ok(Preimage::try_from(p)?))
127 .flatten();
128
129 match preimage_res {
130 Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
131 info!("Lightning payment succeeded! Preimage: {}. Payment hash: {}",
132 preimage.as_hex(), payment.invoice.payment_hash().as_hex());
133
134 self.db.finish_lightning_send(payment_hash, Some(preimage)).await?;
136 self.mark_vtxos_as_spent(&payment.htlc_vtxos).await?;
137 self.movements.finish_movement(
138 payment.movement_id, MovementStatus::Successful,
139 ).await?;
140
141 Ok(Some(preimage))
142 },
143 _ => {
144 error!("Server failed to provide a valid preimage. \
145 Payment hash: {}. Preimage result: {:#?}", payment_hash, preimage_res
146 );
147 Ok(None)
148 }
149 }
150 }
151
152 pub async fn check_lightning_payment(&self, payment_hash: PaymentHash, wait: bool)
179 -> anyhow::Result<Option<Preimage>>
180 {
181 trace!("Checking lightning payment status for payment hash: {}", payment_hash);
182
183 let mut srv = self.require_server()?;
184
185 let payment = self.db.get_lightning_send(payment_hash).await?
186 .context("no lightning send found for payment hash")?;
187
188 if let Some(preimage) = payment.preimage {
190 trace!("Payment already completed with preimage: {}", preimage.as_hex());
191 return Ok(Some(preimage));
192 }
193
194 let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
195 debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
196 "All lightning htlc should have the same policy",
197 );
198 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
199 if policy.payment_hash != payment_hash {
200 bail!("Payment hash mismatch");
201 }
202
203 let req = protos::CheckLightningPaymentRequest {
204 hash: payment_hash.to_vec(),
205 wait,
206 };
207 let response = srv.client.check_lightning_payment(req).await
210 .map(|r| r.into_inner().payment_status);
211
212 let tip = self.chain.tip().await?;
213 let expired = tip > policy.htlc_expiry;
214
215 let should_revoke = match response {
216 Ok(Some(PaymentStatus::Success(status))) => {
217 let preimage_opt = self.process_lightning_send_server_preimage(
218 Some(status.preimage), &payment,
219 ).await?;
220
221 if let Some(preimage) = preimage_opt {
222 return Ok(Some(preimage));
223 } else {
224 trace!("Server said payment is complete, but has no valid preimage: {:?}", preimage_opt);
225 expired
226 }
227 },
228 Ok(Some(PaymentStatus::Failed(_))) => {
229 info!("Payment failed, revoking VTXO");
230 true
231 },
232 Ok(Some(PaymentStatus::Pending(_))) => {
233 trace!("Payment is still pending");
234 expired
235 },
236 Ok(None) | Err(_) => expired,
238 };
239
240 if should_revoke {
241 info!("Revoking HTLC VTXOs for payment {} (tip: {}, expiry: {})",
242 payment_hash, tip, policy.htlc_expiry);
243
244 if let Err(e) = self.process_lightning_revocation(&payment).await {
245 warn!("Failed to revoke VTXO: {}", e);
246
247 let min_expiry = payment.htlc_vtxos.iter()
251 .map(|v| v.vtxo.spec().expiry_height)
252 .min().context("no HTLC VTXOs for expiry check")?;
253
254 if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
255 warn!("Some HTLC VTXOs for payment {} are about to expire soon, marking to exit", payment_hash);
256
257 let vtxos = payment.htlc_vtxos
258 .iter()
259 .map(|v| v.vtxo.clone())
260 .collect::<Vec<_>>();
261 self.exit.write().await.start_exit_for_vtxos(&vtxos).await?;
262
263 let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
264 self.movements.finish_movement_with_update(
265 payment.movement_id,
266 MovementStatus::Failed,
267 MovementUpdate::new()
268 .effective_balance(-payment.amount.to_signed()? + exited.to_signed()?)
269 .exited_vtxos(&vtxos)
270 ).await?;
271 self.db.finish_lightning_send(payment.invoice.payment_hash(), None).await?;
272 }
273
274 return Err(e)
275 }
276 }
277
278 Ok(None)
279 }
280
281 pub async fn pay_lightning_invoice<T>(
288 &self,
289 invoice: T,
290 user_amount: Option<Amount>,
291 ) -> anyhow::Result<LightningSend>
292 where
293 T: TryInto<Invoice>,
294 T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
295 {
296 let invoice = invoice.try_into().context("failed to parse invoice")?;
297 let amount = invoice.get_final_amount(user_amount)?;
298 info!("Sending bolt11 payment of {} to invoice {}", amount, invoice);
299 self.make_lightning_payment(&invoice, invoice.clone().into(), user_amount).await
300 }
301
302 pub async fn pay_lightning_address(
304 &self,
305 addr: &LightningAddress,
306 amount: Amount,
307 comment: Option<impl AsRef<str>>,
308 ) -> anyhow::Result<LightningSend> {
309 let comment = comment.as_ref();
310 let invoice = lnaddr_invoice(addr, amount, comment).await
311 .context("lightning address error")?;
312 info!("Sending {} to lightning address {}", amount, addr);
313 let ret = self.make_lightning_payment(&invoice.into(), addr.clone().into(), None).await
314 .context("bolt11 payment error")?;
315 info!("Paid invoice {}", ret.invoice);
316 Ok(ret)
317 }
318
319 pub async fn pay_lightning_offer(
321 &self,
322 offer: Offer,
323 user_amount: Option<Amount>,
324 ) -> anyhow::Result<LightningSend> {
325 let mut srv = self.require_server()?;
326
327 let offer_bytes = {
328 let mut bytes = Vec::new();
329 offer.write(&mut bytes).context("failed to serialize BOLT12 offer")?;
330 bytes
331 };
332
333 let req = protos::FetchBolt12InvoiceRequest {
334 offer: offer_bytes,
335 amount_sat: user_amount.map(|a| a.to_sat()),
336 };
337
338 if let Some(amt) = user_amount {
339 info!("Sending bolt12 payment of {} (user amount) to offer {}", amt, offer);
340 } else if let Some(amt) = offer.amount() {
341 info!("Sending bolt12 payment of {:?} (invoice amount) to offer {}", amt, offer);
342 } else {
343 warn!("Paying offer without amount nor user amount provided: {}", offer);
344 }
345
346 let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
347 let invoice = Bolt12Invoice::try_from(resp.invoice)
348 .map_err(|e| anyhow!("invalid invoice: {:?}", e))?;
349
350 invoice.validate_issuance(&offer)
351 .context("invalid BOLT12 invoice received from offer")?;
352
353 let ret = self.make_lightning_payment(&invoice.into(), offer.into(), None).await
354 .context("bolt12 payment error")?;
355 info!("Paid invoice: {:?}", ret.invoice);
356
357 Ok(ret)
358 }
359
360 pub async fn make_lightning_payment(
398 &self,
399 invoice: &Invoice,
400 original_payment_method: PaymentMethod,
401 user_amount: Option<Amount>,
402 ) -> anyhow::Result<LightningSend> {
403 if !original_payment_method.is_lightning() && !original_payment_method.is_custom() {
404 bail!("Invalid original payment method for lightning payment");
405 }
406 let mut srv = self.require_server()?;
407
408 let properties = self.db.read_properties().await?.context("Missing config")?;
409 if invoice.network() != properties.network {
410 bail!("Invoice is for wrong network: {}", invoice.network());
411 }
412
413 let lightning_send = self.db.get_lightning_send(invoice.payment_hash()).await?;
414 if lightning_send.is_some() {
415 bail!("Invoice has already been paid");
416 }
417
418 invoice.check_signature()?;
419
420 let amount = invoice.get_final_amount(user_amount)?;
421 if amount < P2TR_DUST {
422 bail!("Sent amount must be at least {}", P2TR_DUST);
423 }
424
425 let (change_keypair, _) = self.derive_store_next_keypair().await?;
426
427 let inputs = self.select_vtxos_to_cover(amount).await
428 .context("Could not find enough suitable VTXOs to cover lightning payment")?;
429
430 let mut secs = Vec::with_capacity(inputs.len());
431 let mut pubs = Vec::with_capacity(inputs.len());
432 let mut keypairs = Vec::with_capacity(inputs.len());
433 let mut input_ids = Vec::with_capacity(inputs.len());
434 for input in inputs.iter() {
435 let keypair = self.get_vtxo_key(input).await?;
436 let (s, p) = musig::nonce_pair(&keypair);
437 secs.push(s);
438 pubs.push(p);
439 keypairs.push(keypair);
440 input_ids.push(input.id());
441 }
442
443 let req = protos::LightningPayHtlcCosignRequest {
444 invoice: invoice.to_string(),
445 user_amount_sat: user_amount.map(|a| a.to_sat()),
446 input_vtxo_ids: input_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
447 user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
448 user_pubkey: change_keypair.public_key().serialize().to_vec(),
449 };
450
451 let resp = srv.client.request_lightning_pay_htlc_cosign(req).await
452 .context("htlc request failed")?.into_inner();
453
454 let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
455 .collect::<Result<Vec<_>, _>>()?;
456 let policy = VtxoPolicy::deserialize(&resp.policy)?;
457
458 let pay_req = match &policy {
459 VtxoPolicy::ServerHtlcSend(policy) => {
460 ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
461 ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
462 VtxoRequest { amount: amount, policy: policy.clone().into() }
464 },
465 _ => bail!("invalid policy returned from server"),
466 };
467
468 let builder = ArkoorPackageBuilder::new(
469 &inputs, &pubs, pay_req, Some(change_keypair.public_key()),
470 )?;
471
472 ensure!(builder.verify_cosign_response(&cosign_resp),
473 "invalid arkoor cosignature received from server",
474 );
475
476 let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
477
478 let mut effective_balance = Amount::ZERO;
480 for vtxo in &htlc_vtxos {
481 self.validate_vtxo(vtxo).await?;
482 effective_balance += vtxo.amount();
483 }
484
485 let movement_id = self.movements.new_movement_with_update(
486 Subsystem::LIGHTNING_SEND,
487 LightningSendMovement::Send.to_string(),
488 MovementUpdate::new()
489 .intended_balance(-amount.to_signed()?)
490 .effective_balance(-effective_balance.to_signed()?)
491 .consumed_vtxos(&inputs)
492 .sent_to([MovementDestination::new(original_payment_method, amount)])
493 ).await?;
494 self.store_locked_vtxos(&htlc_vtxos, Some(movement_id)).await?;
495 self.mark_vtxos_as_spent(&input_ids).await?;
496
497 if let Some(ref change) = change_vtxo {
499 let last_input = inputs.last().context("no inputs provided")?;
500 let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
501 let tx = tx.with_context(|| {
502 format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
503 })?;
504 change.validate(&tx).context("invalid lightning change vtxo")?;
505 self.store_spendable_vtxos([change]).await?;
506 }
507
508 self.movements.update_movement(
509 movement_id,
510 MovementUpdate::new()
511 .produced_vtxo_if_some(change_vtxo)
512 .metadata(LightningMovement::metadata(invoice.payment_hash(), &htlc_vtxos))
513 ).await?;
514
515 let lightning_send = self.db.store_new_pending_lightning_send(
516 &invoice, &amount,
517 &htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(),
518 movement_id,
519 ).await?;
520
521 let req = protos::InitiateLightningPaymentRequest {
522 invoice: invoice.to_string(),
523 htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
524 };
525
526 srv.client.initiate_lightning_payment(req).await?;
527
528 Ok(lightning_send)
529 }
530}