1use std::fmt;
2
3use anyhow::Context;
4use bitcoin::Amount;
5use bitcoin::hex::DisplayHex;
6use lightning::util::ser::Writeable;
7use lightning_invoice::Bolt11Invoice;
8use lnurllib::lightning_address::LightningAddress;
9use log::{debug, error, info, trace, warn};
10use server_rpc::protos;
11
12use ark::arkoor::ArkoorPackageBuilder;
13use ark::lightning::{Bolt12Invoice, Bolt12InvoiceExt, Invoice, Offer, Preimage};
14use ark::{ProtocolEncoding, VtxoPolicy, VtxoRequest, musig};
15use bitcoin_ext::P2TR_DUST;
16
17use crate::Wallet;
18use crate::lightning::lnaddr_invoice;
19use crate::movement::{MovementDestination, MovementStatus};
20use crate::movement::update::MovementUpdate;
21use crate::persist::models::LightningSend;
22use crate::subsystem::{BarkSubsystem, LightningMovement, LightningSendMovement};
23
24
25impl Wallet {
26 async fn process_lightning_revocation(&self, payment: &LightningSend) -> anyhow::Result<()> {
27 let mut srv = self.require_server()?;
28 let htlc_vtxos = payment.htlc_vtxos.clone().into_iter()
29 .map(|v| v.vtxo).collect::<Vec<_>>();
30
31 info!("Processing {} HTLC VTXOs for revocation", htlc_vtxos.len());
32
33 let mut secs = Vec::with_capacity(htlc_vtxos.len());
34 let mut pubs = Vec::with_capacity(htlc_vtxos.len());
35 let mut keypairs = Vec::with_capacity(htlc_vtxos.len());
36 for input in htlc_vtxos.iter() {
37 let keypair = self.get_vtxo_key(&input)?;
38 let (s, p) = musig::nonce_pair(&keypair);
39 secs.push(s);
40 pubs.push(p);
41 keypairs.push(keypair);
42 }
43
44 let revocation = ArkoorPackageBuilder::new_htlc_revocation(&htlc_vtxos, &pubs)?;
45
46 let req = protos::RevokeLightningPayHtlcRequest {
47 htlc_vtxo_ids: revocation.arkoors.iter()
48 .map(|i| i.input.id().to_bytes().to_vec())
49 .collect(),
50 user_nonces: revocation.arkoors.iter()
51 .map(|i| i.user_nonce.serialize().to_vec())
52 .collect(),
53 };
54 let cosign_resp: Vec<_> = srv.client.request_lightning_pay_htlc_revocation(req).await?
55 .into_inner().try_into().context("invalid server cosign response")?;
56 ensure!(revocation.verify_cosign_response(&cosign_resp),
57 "invalid arkoor cosignature received from server",
58 );
59
60 let (vtxos, _) = revocation.build_vtxos(&cosign_resp, &keypairs, secs)?;
61 let mut revoked = Amount::ZERO;
62 for vtxo in &vtxos {
63 info!("Got revocation VTXO: {}: {}", vtxo.id(), vtxo.amount());
64 revoked += vtxo.amount();
65 }
66
67 let count = vtxos.len();
68 self.movements.update_movement(
69 payment.movement_id,
70 MovementUpdate::new()
71 .effective_balance(-payment.amount.to_signed()? + revoked.to_signed()?)
72 .produced_vtxos(&vtxos)
73 ).await?;
74 self.store_spendable_vtxos(&vtxos)?;
75 self.mark_vtxos_as_spent(&htlc_vtxos)?;
76 self.movements.finish_movement(payment.movement_id, MovementStatus::Failed).await?;
77
78 self.db.remove_lightning_send(payment.invoice.payment_hash())?;
79
80 info!("Revoked {} HTLC VTXOs", count);
81
82 Ok(())
83 }
84
85 async fn process_lightning_send_server_preimage(
100 &self,
101 preimage: Option<Vec<u8>>,
102 payment: &LightningSend,
103 ) -> anyhow::Result<Option<Preimage>> {
104 let payment_hash = payment.invoice.payment_hash();
105 let preimage_res = preimage
106 .context("preimage is missing")
107 .map(|p| Ok(Preimage::try_from(p)?))
108 .flatten();
109
110 match preimage_res {
111 Ok(preimage) if preimage.compute_payment_hash() == payment_hash => {
112 info!("Lightning payment succeeded! Preimage: {}. Payment hash: {}",
113 preimage.as_hex(), payment.invoice.payment_hash().as_hex());
114
115 self.db.finish_lightning_send(payment_hash, Some(preimage))?;
117 self.mark_vtxos_as_spent(&payment.htlc_vtxos)?;
118 self.movements.finish_movement(payment.movement_id,
119 MovementStatus::Finished).await?;
120
121 Ok(Some(preimage))
122 },
123 _ => {
124 error!("Server failed to provide a valid preimage. \
125 Payment hash: {}. Preimage result: {:#?}", payment_hash, preimage_res
126 );
127 Ok(None)
128 }
129 }
130 }
131
132 pub async fn check_lightning_payment(&self, payment: &LightningSend)
159 -> anyhow::Result<Option<Preimage>>
160 {
161 let mut srv = self.require_server()?;
162 let tip = self.chain.tip().await?;
163
164 let payment_hash = payment.invoice.payment_hash();
165
166 let policy = payment.htlc_vtxos.first().context("no vtxo provided")?.vtxo.policy();
167 debug_assert!(payment.htlc_vtxos.iter().all(|v| v.vtxo.policy() == policy),
168 "All lightning htlc should have the same policy",
169 );
170 let policy = policy.as_server_htlc_send().context("VTXO is not an HTLC send")?;
171 if policy.payment_hash != payment_hash {
172 bail!("Payment hash mismatch");
173 }
174
175 let req = protos::CheckLightningPaymentRequest {
176 hash: policy.payment_hash.to_vec(),
177 wait: false,
178 };
179 let res = srv.client.check_lightning_payment(req).await?.into_inner();
180
181 let payment_status = protos::PaymentStatus::try_from(res.status)?;
182
183 let should_revoke = match payment_status {
184 protos::PaymentStatus::Failed => {
185 info!("Payment failed ({}): revoking VTXO", res.progress_message);
186 true
187 },
188 protos::PaymentStatus::Pending => {
189 if tip > policy.htlc_expiry {
190 trace!("Payment is still pending, but HTLC is expired (tip: {}, \
191 expiry: {}): revoking VTXO", tip, policy.htlc_expiry);
192 true
193 } else {
194 trace!("Payment is still pending and HTLC is not expired (tip: {}, \
195 expiry: {}): doing nothing for now", tip, policy.htlc_expiry);
196 false
197 }
198 },
199 protos::PaymentStatus::Complete => {
200 let preimage_opt = self.process_lightning_send_server_preimage(
201 res.payment_preimage, &payment,
202 ).await?;
203
204 if let Some(preimage) = preimage_opt {
205 return Ok(Some(preimage));
206 } else {
207 if tip > policy.htlc_expiry {
208 trace!("Completed payment has no valid preimage and HTLC is \
209 expired (tip: {}, expiry: {}): revoking VTXO", tip, policy.htlc_expiry);
210 true
211 } else {
212 trace!("Completed payment has no valid preimage, but HTLC is \
213 not expired (tip: {}, expiry: {}): doing nothing for now", tip, policy.htlc_expiry);
214 false
215 }
216 }
217 },
218 };
219
220 if should_revoke {
221 if let Err(e) = self.process_lightning_revocation(payment).await {
222 warn!("Failed to revoke VTXO: {}", e);
223
224 let min_expiry = payment.htlc_vtxos.iter()
228 .map(|v| v.vtxo.spec().expiry_height).min().unwrap();
229
230 if tip > min_expiry.saturating_sub(self.config().vtxo_refresh_expiry_threshold) {
231 warn!("Some VTXO is about to expire soon, marking to exit");
232 let vtxos = payment.htlc_vtxos
233 .iter()
234 .map(|v| v.vtxo.clone())
235 .collect::<Vec<_>>();
236 self.exit.write().await.mark_vtxos_for_exit(&vtxos).await?;
237
238 let exited = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
239 self.movements.update_movement(
240 payment.movement_id,
241 MovementUpdate::new()
242 .effective_balance(-payment.amount.to_signed()? + exited.to_signed()?)
243 .exited_vtxos(&vtxos)
244 ).await?;
245 self.movements.finish_movement(
246 payment.movement_id, MovementStatus::Failed,
247 ).await?;
248 self.db.finish_lightning_send(payment.invoice.payment_hash(), None)?;
249 }
250 }
251 }
252
253 Ok(None)
254 }
255
256 pub async fn pay_lightning_invoice<T>(
259 &self,
260 invoice: T,
261 user_amount: Option<Amount>,
262 ) -> anyhow::Result<Preimage>
263 where
264 T: TryInto<Invoice>,
265 T::Error: std::error::Error + fmt::Display + Send + Sync + 'static,
266 {
267 let mut srv = self.require_server()?;
268
269 let properties = self.db.read_properties()?.context("Missing config")?;
270
271 let invoice = invoice.try_into().context("failed to parse invoice")?;
272 if invoice.network() != properties.network {
273 bail!("Invoice is for wrong network: {}", invoice.network());
274 }
275
276 if self.db.get_lightning_send(invoice.payment_hash())?.is_some() {
277 bail!("Invoice has already been paid");
278 }
279
280 invoice.check_signature()?;
281
282 let amount = invoice.get_final_amount(user_amount)?;
283 if amount < P2TR_DUST {
284 bail!("Sent amount must be at least {}", P2TR_DUST);
285 }
286
287 let (change_keypair, _) = self.derive_store_next_keypair()?;
288
289 let inputs = self.select_vtxos_to_cover(amount, None)
290 .context("Could not find enough suitable VTXOs to cover lightning payment")?;
291
292 let mut secs = Vec::with_capacity(inputs.len());
293 let mut pubs = Vec::with_capacity(inputs.len());
294 let mut keypairs = Vec::with_capacity(inputs.len());
295 let mut input_ids = Vec::with_capacity(inputs.len());
296 for input in inputs.iter() {
297 let keypair = self.get_vtxo_key(&input)?;
298 let (s, p) = musig::nonce_pair(&keypair);
299 secs.push(s);
300 pubs.push(p);
301 keypairs.push(keypair);
302 input_ids.push(input.id());
303 }
304
305 let req = protos::LightningPayHtlcCosignRequest {
306 invoice: invoice.to_string(),
307 user_amount_sat: user_amount.map(|a| a.to_sat()),
308 input_vtxo_ids: input_ids.iter().map(|v| v.to_bytes().to_vec()).collect(),
309 user_nonces: pubs.iter().map(|p| p.serialize().to_vec()).collect(),
310 user_pubkey: change_keypair.public_key().serialize().to_vec(),
311 };
312
313 let resp = srv.client.request_lightning_pay_htlc_cosign(req).await
314 .context("htlc request failed")?.into_inner();
315
316 let cosign_resp = resp.sigs.into_iter().map(|i| i.try_into())
317 .collect::<Result<Vec<_>, _>>()?;
318 let policy = VtxoPolicy::deserialize(&resp.policy)?;
319
320 let pay_req = match policy {
321 VtxoPolicy::ServerHtlcSend(policy) => {
322 ensure!(policy.user_pubkey == change_keypair.public_key(), "user pubkey mismatch");
323 ensure!(policy.payment_hash == invoice.payment_hash(), "payment hash mismatch");
324 VtxoRequest { amount: amount, policy: policy.into() }
326 },
327 _ => bail!("invalid policy returned from server"),
328 };
329
330 let builder = ArkoorPackageBuilder::new(
331 &inputs, &pubs, pay_req, Some(change_keypair.public_key()),
332 )?;
333
334 ensure!(builder.verify_cosign_response(&cosign_resp),
335 "invalid arkoor cosignature received from server",
336 );
337
338 let (htlc_vtxos, change_vtxo) = builder.build_vtxos(&cosign_resp, &keypairs, secs)?;
339
340 let mut effective_balance = Amount::ZERO;
342 for vtxo in &htlc_vtxos {
343 self.validate_vtxo(vtxo).await?;
344 effective_balance += vtxo.amount();
345 }
346
347 let movement_id = self.movements.new_movement(
348 self.subsystem_ids[&BarkSubsystem::LightningSend],
349 LightningSendMovement::Send.to_string(),
350 ).await?;
351 self.movements.update_movement(
352 movement_id,
353 MovementUpdate::new()
354 .intended_balance(-amount.to_signed()?)
355 .effective_balance(-effective_balance.to_signed()?)
356 .consumed_vtxos(&inputs)
357 .sent_to([MovementDestination::new(invoice.to_string(), amount)])
358 ).await?;
359 self.store_locked_vtxos(&htlc_vtxos, Some(movement_id))?;
360 self.mark_vtxos_as_spent(&input_ids)?;
361
362 if let Some(ref change) = change_vtxo {
364 let last_input = inputs.last().context("no inputs provided")?;
365 let tx = self.chain.get_tx(&last_input.chain_anchor().txid).await?;
366 let tx = tx.with_context(|| {
367 format!("input vtxo chain anchor not found for lightning change vtxo: {}", last_input.chain_anchor().txid)
368 })?;
369 change.validate(&tx).context("invalid lightning change vtxo")?;
370 self.store_spendable_vtxos([change])?;
371 }
372
373 self.movements.update_movement(
374 movement_id,
375 MovementUpdate::new()
376 .produced_vtxo_if_some(change_vtxo)
377 .metadata(LightningMovement::htlc_metadata(&htlc_vtxos)?)
378 ).await?;
379
380 let payment = self.db.store_new_pending_lightning_send(
381 &invoice, &amount, &htlc_vtxos.iter().map(|v| v.id()).collect::<Vec<_>>(), movement_id,
382 )?;
383
384 let req = protos::InitiateLightningPaymentRequest {
385 invoice: invoice.to_string(),
386 htlc_vtxo_ids: htlc_vtxos.iter().map(|v| v.id().to_bytes().to_vec()).collect(),
387 wait: true,
388 };
389
390 let res = srv.client.initiate_lightning_payment(req).await?.into_inner();
391 debug!("Progress update: {}", res.progress_message);
392
393 let preimage_opt = self.process_lightning_send_server_preimage(
394 res.payment_preimage, &payment,
395 ).await?;
396
397 if let Some(preimage) = preimage_opt {
398 return Ok(preimage);
399 } else {
400 self.process_lightning_revocation(&payment).await?;
401 bail!("Payment failed, but got revocation vtxos: {}", res.progress_message);
402 }
403 }
404
405 pub async fn pay_lightning_address(
407 &self,
408 addr: &LightningAddress,
409 amount: Amount,
410 comment: Option<&str>,
411 ) -> anyhow::Result<(Bolt11Invoice, Preimage)> {
412 let invoice = lnaddr_invoice(addr, amount, comment).await
413 .context("lightning address error")?;
414 info!("Attempting to pay invoice {}", invoice);
415 let preimage = self.pay_lightning_invoice(invoice.clone(), None).await
416 .context("bolt11 payment error")?;
417 Ok((invoice, preimage))
418 }
419
420 pub async fn pay_lightning_offer(
422 &self,
423 offer: Offer,
424 amount: Option<Amount>,
425 ) -> anyhow::Result<(Bolt12Invoice, Preimage)> {
426 let mut srv = self.require_server()?;
427
428 let offer_bytes = {
429 let mut bytes = Vec::new();
430 offer.write(&mut bytes).unwrap();
431 bytes
432 };
433
434 let req = protos::FetchBolt12InvoiceRequest {
435 offer: offer_bytes,
436 amount_sat: amount.map(|a| a.to_sat()),
437 };
438
439 let resp = srv.client.fetch_bolt12_invoice(req).await?.into_inner();
440
441 let invoice = Bolt12Invoice::try_from(resp.invoice)
442 .map_err(|_| anyhow::anyhow!("invalid invoice"))?;
443
444 invoice.validate_issuance(offer)?;
445
446 let preimage = self.pay_lightning_invoice(invoice.clone(), None).await
447 .context("bolt11 payment error")?;
448 Ok((invoice, preimage))
449 }
450
451}