1use std::str::FromStr;
2use std::time::Duration;
3
4use anyhow::Context;
5use ark::arkoor::package::ArkoorPackageBuilder;
6use bitcoin::{Amount, SignedAmount};
7use bitcoin::hex::DisplayHex;
8use futures::StreamExt;
9use lightning_invoice::Bolt11Invoice;
10use log::{trace, debug, info, warn};
11
12use ark::{ProtocolEncoding, Vtxo, VtxoPolicy};
13use ark::attestations::{LightningReceiveAttestation};
14use ark::fees::validate_and_subtract_fee;
15use ark::lightning::{Bolt11InvoiceExt, PaymentHash, Preimage};
16use bitcoin_ext::{BlockDelta, BlockHeight};
17use server_rpc::protos;
18use server_rpc::protos::prepare_lightning_receive_claim_request::LightningReceiveAntiDos;
19
20use crate::subsystem::{LightningMovement, LightningReceiveMovement, Subsystem};
21use crate::{Wallet, error};
22use crate::movement::{MovementDestination, MovementStatus};
23use crate::movement::update::MovementUpdate;
24use crate::persist::models::LightningReceive;
25
26const LIGHTNING_RECEIVE_LOCK_PREFIX: &str = "lightning_receive";
27
28const LIGHTNING_PREPARE_CLAIM_DELTA: BlockDelta = 2;
31
32const CLAIM_RETRY_BACKOFF_INITIAL: Duration = Duration::from_secs(2);
37const CLAIM_RETRY_BACKOFF_MAX: Duration = Duration::from_secs(30);
38
39fn validate_bolt11_payment_hash(
40 invoice: &Bolt11Invoice,
41 expected_payment_hash: PaymentHash,
42) -> anyhow::Result<()> {
43 let invoice_payment_hash = PaymentHash::from(invoice);
44 ensure!(
45 invoice_payment_hash == expected_payment_hash,
46 "Ark server returned invoice with payment hash {}, expected {}",
47 invoice_payment_hash,
48 expected_payment_hash,
49 );
50
51 Ok(())
52}
53
54impl Wallet {
55 pub async fn pending_lightning_receives(&self) -> anyhow::Result<Vec<LightningReceive>> {
57 Ok(self.inner.db.get_all_pending_lightning_receives().await?)
58 }
59
60 pub async fn claimable_lightning_receive_balance(&self) -> anyhow::Result<Amount> {
63 let receives = self.pending_lightning_receives().await?;
64
65 let mut total = Amount::ZERO;
66 for receive in receives {
67 total += receive.htlc_vtxos.iter().map(|v| v.amount()).sum::<Amount>();
68 }
69
70 Ok(total)
71 }
72
73 pub async fn bolt11_invoice(
80 &self,
81 amount: Amount,
82 description: Option<String>,
83 ) -> anyhow::Result<Bolt11Invoice> {
84 if amount == Amount::ZERO {
85 bail!("Cannot create invoice for 0 sats (this would create an explicit 0 sat invoice, not an any-amount invoice)");
86 }
87
88 let (mut srv, ark_info) = self.require_server().await?;
89 let config = self.config();
90
91 let fee = ark_info.fees.lightning_receive.calculate(amount).context("fee overflowed")?;
93 validate_and_subtract_fee(amount, fee)?;
94
95 let requested_min_cltv_delta = ark_info.vtxo_exit_delta +
100 ark_info.htlc_expiry_delta +
101 config.vtxo_exit_margin +
102 config.htlc_recv_claim_delta +
103 LIGHTNING_PREPARE_CLAIM_DELTA;
104
105 if requested_min_cltv_delta > ark_info.max_user_invoice_cltv_delta {
106 bail!("HTLC CLTV delta ({}) is greater than Server's max HTLC recv CLTV delta: {}",
107 requested_min_cltv_delta,
108 ark_info.max_user_invoice_cltv_delta,
109 );
110 }
111
112 let preimage = Preimage::random();
113 let payment_hash = preimage.compute_payment_hash();
114 info!("Start bolt11 board with preimage / payment hash: {} / {}",
115 preimage.as_hex(), payment_hash.as_hex());
116
117 let mailbox_kp = self.inner.seed.to_mailbox_keypair();
118 let mailbox_id = ark::mailbox::MailboxIdentifier::from_pubkey(mailbox_kp.public_key());
119
120 let req = protos::StartLightningReceiveRequest {
121 payment_hash: payment_hash.to_vec(),
122 amount_sat: amount.to_sat(),
123 min_cltv_delta: requested_min_cltv_delta as u32,
124 mailbox_id: Some(mailbox_id.serialize()),
125 description,
126 };
127
128 let resp = srv.client.start_lightning_receive(req).await?.into_inner();
129 info!("Ark Server is ready to receive LN payment to invoice: {}.", resp.bolt11);
130
131 let invoice = Bolt11Invoice::from_str(&resp.bolt11)
132 .context("invalid bolt11 invoice returned by Ark server")?;
133 validate_bolt11_payment_hash(&invoice, payment_hash)?;
134
135 self.inner.db.store_lightning_receive(
136 payment_hash,
137 preimage,
138 &invoice,
139 requested_min_cltv_delta,
140 ).await?;
141
142 Ok(invoice)
143 }
144
145 pub async fn lightning_receive_status(
147 &self,
148 payment: impl Into<PaymentHash>,
149 ) -> anyhow::Result<Option<LightningReceive>> {
150 Ok(self.inner.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?)
151 }
152
153 pub async fn attempt_lightning_receive_exit(
161 &self,
162 payment: impl Into<PaymentHash>,
163 ) -> anyhow::Result<()> {
164 let receive = self.inner.db.fetch_lightning_receive_by_payment_hash(payment.into()).await?
165 .context("no pending lightning receive found for payment hash")?;
166 if receive.preimage_revealed_at.is_none() {
167 bail!("preimage must be revealed before attempting to exit");
168 }
169 if receive.htlc_vtxos.is_empty() {
170 bail!("Nothing to exit, no htlcs have been created yet!");
171 }
172 self.exit_lightning_receive(&receive).await
173 }
174
175 async fn claim_lightning_receive(
196 &self,
197 receive: &mut LightningReceive,
198 ) -> anyhow::Result<()> {
199 let movement_id = receive.movement_id
200 .context("No movement created for lightning receive")?;
201 let (mut srv, _) = self.require_server().await?;
202
203 ensure!(!receive.htlc_vtxos.is_empty(), "no HTLC VTXOs set on record yet");
206 let mut input_ids = receive.htlc_vtxos.iter().map(|v| v.vtxo.id()).collect::<Vec<_>>();
207 input_ids.sort();
208 let inputs = self.inner.db.get_full_vtxos(&input_ids).await
209 .context("failed to hydrate htlc input vtxos")?;
210
211 let mut keypairs = Vec::with_capacity(inputs.len());
212 for v in &inputs {
213 keypairs.push(self.get_vtxo_key(v).await?);
214 }
215
216 let (claim_keypair, _) = self.derive_store_next_keypair().await?;
218 let receive_policy = VtxoPolicy::new_pubkey(claim_keypair.public_key());
219
220 trace!("ln arkoor builder params: inputs: {:?}; policy: {:?}", input_ids, receive_policy);
221 let builder = ArkoorPackageBuilder::new_claim_all_with_checkpoints(
222 inputs,
223 receive_policy.clone(),
224 ).context("creating claim arkoor builder failed")?;
225 let builder = builder.generate_user_nonces(&keypairs)
226 .context("arkoor nonce generation for claim failed")?;
227
228 info!("Claiming arkoor against payment preimage");
229 self.inner.db.set_preimage_revealed(receive.payment_hash).await?;
230 *receive = self.inner.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
235 .context("Database error")?
236 .context("Receive not found")?;
237 let package_cosign_request = protos::ArkoorPackageCosignRequest::from(
238 builder.cosign_request(),
239 );
240 let resp = srv.client.claim_lightning_receive(protos::ClaimLightningReceiveRequest {
241 payment_hash: receive.payment_hash.to_byte_array().to_vec(),
242 payment_preimage: receive.payment_preimage.to_vec(),
243 cosign_request: Some(package_cosign_request),
244 }).await?.into_inner();
245 let cosign_resp = resp.try_into().context("invalid cosign response")?;
246
247 let outputs = builder.user_cosign(&keypairs, cosign_resp)
248 .context("claim arkoor cosign failed with user response")?
249 .build_signed_vtxos();
250
251 self.register_vtxo_transactions_with_server(&outputs).await?;
253
254 let mut effective_balance = Amount::ZERO;
255 for vtxo in &outputs {
256 trace!("Validating Lightning receive claim VTXO {}: {}",
261 vtxo.id(), vtxo.serialize_hex(),
262 );
263 self.validate_vtxo(vtxo).await
264 .context("invalid arkoor from lightning receive")?;
265 effective_balance += vtxo.amount();
266 }
267
268 self.store_spendable_vtxos(&outputs).await?;
269 self.mark_vtxos_as_spent(&receive.htlc_vtxos).await?;
270
271 info!("Got arkoors from lightning: {}",
272 outputs.iter().map(|v| v.id().to_string()).collect::<Vec<_>>().join(", ")
273 );
274
275 self.inner.movements.finish_movement_with_update(
276 movement_id,
277 MovementStatus::Successful,
278 MovementUpdate::new()
279 .effective_balance(effective_balance.to_signed()?)
280 .produced_vtxos(&outputs)
281 ).await?;
282
283 self.inner.db.finish_pending_lightning_receive(receive.payment_hash).await?;
284 *receive = self.inner.db.fetch_lightning_receive_by_payment_hash(receive.payment_hash).await
285 .context("Database error")?
286 .context("Receive not found")?;
287
288 Ok(())
289 }
290
291 async fn compute_lightning_receive_anti_dos(
292 &self,
293 payment_hash: PaymentHash,
294 token: Option<&str>,
295 ) -> anyhow::Result<LightningReceiveAntiDos> {
296 Ok(if let Some(token) = token {
297 LightningReceiveAntiDos::Token(token.to_string())
298 } else {
299 let vtxo = self.select_vtxos_to_cover(Amount::ONE_SAT).await
301 .and_then(|vtxos| vtxos.into_iter().next()
302 .context("have no spendable vtxo to prove ownership of")
303 )?;
304 let vtxo_keypair = self.get_vtxo_key(&vtxo).await.expect("owned vtxo should be in database");
305 let attestation = LightningReceiveAttestation::new(payment_hash, vtxo.id(), &vtxo_keypair);
306 LightningReceiveAntiDos::InputVtxo(protos::InputVtxo {
307 vtxo_id: vtxo.id().to_bytes().to_vec(),
308 attestation: attestation.serialize(),
309 })
310 })
311 }
312
313 async fn check_lightning_receive(
344 &self,
345 payment_hash: PaymentHash,
346 wait: bool,
347 token: Option<&str>,
348 ) -> anyhow::Result<Option<LightningReceive>> {
349 let (mut srv, ark_info) = self.require_server().await?;
350 let current_height = self.inner.chain.tip().await?;
351
352 let mut receive = self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
353 .context("no pending lightning receive found for payment hash, might already be claimed")?;
354
355 if !receive.htlc_vtxos.is_empty() {
357 return Ok(Some(receive))
358 }
359
360 trace!("Requesting updates for ln-receive to server with for wait={} and hash={}", wait, payment_hash);
361 let sub = srv.client.check_lightning_receive(protos::CheckLightningReceiveRequest {
362 hash: payment_hash.to_byte_array().to_vec(), wait,
363 }).await?.into_inner();
364
365
366 let status = protos::LightningReceiveStatus::try_from(sub.status)
367 .with_context(|| format!("unknown payment status: {}", sub.status))?;
368
369 debug!("Received status {:?} for {}", status, payment_hash);
370 match status {
371 protos::LightningReceiveStatus::Accepted |
373 protos::LightningReceiveStatus::HtlcsReady => {},
374 protos::LightningReceiveStatus::Created => {
375 return Ok(None);
376 },
377 protos::LightningReceiveStatus::Settled => bail!("payment already settled"),
378 protos::LightningReceiveStatus::Canceled => {
379 warn!("payment was canceled. removing pending lightning receive");
380 self.handle_failed_lightning_receive(&receive).await?;
381 return Ok(None);
382 },
383 }
384
385 let lightning_receive_anti_dos = match self.compute_lightning_receive_anti_dos(
386 payment_hash, token,
387 ).await {
388 Ok(anti_dos) => Some(anti_dos),
389 Err(e) => {
390 info!("Could not compute anti-dos: {e:#}. Trying without");
391 None
392 },
393 };
394
395 let htlc_recv_expiry = current_height + receive.htlc_recv_cltv_delta as BlockHeight;
396
397 let (next_keypair, _) = self.derive_store_next_keypair().await?;
398 let req = protos::PrepareLightningReceiveClaimRequest {
399 payment_hash: receive.payment_hash.to_vec(),
400 user_pubkey: next_keypair.public_key().serialize().to_vec(),
401 htlc_recv_expiry,
402 lightning_receive_anti_dos,
403 };
404 let res = srv.client.prepare_lightning_receive_claim(req).await
405 .context("error preparing lightning receive claim")?.into_inner();
406 let vtxos = res.htlc_vtxos.into_iter()
407 .map(|b| Vtxo::deserialize(&b))
408 .collect::<Result<Vec<_>, _>>()
409 .context("invalid htlc vtxos from server")?;
410
411 let mut htlc_amount = Amount::ZERO;
413 for vtxo in &vtxos {
414 trace!("Received HTLC VTXO {} from server: {}", vtxo.id(), vtxo.serialize_hex());
415 self.validate_vtxo(vtxo).await
416 .context("received invalid HTLC VTXO from server")?;
417 htlc_amount += vtxo.amount();
418
419 if let VtxoPolicy::ServerHtlcRecv(p) = vtxo.policy() {
420 if p.payment_hash != receive.payment_hash {
421 bail!("invalid payment hash on HTLC VTXOs received from server: {}",
422 p.payment_hash,
423 );
424 }
425 if p.user_pubkey != next_keypair.public_key() {
426 bail!("invalid pubkey on HTLC VTXOs received from server: {}", p.user_pubkey);
427 }
428 if p.htlc_expiry < htlc_recv_expiry {
429 bail!("HTLC VTXO expiry height is less than requested: Requested {}, received {}", htlc_recv_expiry, p.htlc_expiry);
430 }
431 } else {
432 bail!("invalid HTLC VTXO policy: {:?}", vtxo.policy());
433 }
434 }
435
436 let invoice_amount = receive.invoice.get_payment_amount(None)
440 .context("ln receive invoice should have amount")?;
441 let server_received_amount = res.receive.map(|r| Amount::from_sat(r.amount_sat));
442 let fee = {
443 let fee = server_received_amount
444 .and_then(|a| ark_info.fees.lightning_receive.calculate(a));
445 match (server_received_amount, fee) {
446 (Some(amount), Some(fee)) if htlc_amount + fee == amount => {
447 fee
449 },
450 _ => {
451 ark_info.fees.lightning_receive.calculate(invoice_amount)
456 .expect("we previously validated this")
457 }
458 }
459 };
460 let received = htlc_amount + fee;
461 ensure!(received >= invoice_amount,
462 "Server didn't return enough VTXOs to cover invoice amount"
463 );
464
465 let movement_id = if let Some(movement_id) = receive.movement_id {
466 movement_id
467 } else {
468 self.inner.movements.new_movement_with_update(
469 Subsystem::LIGHTNING_RECEIVE,
470 LightningReceiveMovement::Receive.to_string(),
471 MovementUpdate::new()
472 .intended_balance(invoice_amount.to_signed()?)
473 .effective_balance(htlc_amount.to_signed()?)
474 .fee(fee)
475 .metadata(LightningMovement::metadata(
476 receive.payment_hash, &vtxos, Some(receive.payment_preimage),
477 ))
478 .received_on(
479 [MovementDestination::new(receive.invoice.clone().into(), received)],
480 ),
481 ).await?
482 };
483 self.store_locked_vtxos(
484 &vtxos,
485 Some(crate::vtxo::VtxoLockHolder::Movement { id: movement_id }),
486 ).await?;
487
488 let vtxo_ids = vtxos.iter().map(|v| v.id()).collect::<Vec<_>>();
489 self.inner.db.update_lightning_receive(payment_hash, &vtxo_ids, movement_id).await?;
490
491 let mut wallet_vtxos = vec![];
492 for vtxo in vtxos {
493 let v = self.inner.db.get_wallet_vtxo(vtxo.id()).await?
494 .context("Failed to get wallet VTXO for lightning receive")?;
495 wallet_vtxos.push(v);
496 }
497
498 receive.htlc_vtxos = wallet_vtxos;
499 receive.movement_id = Some(movement_id);
500
501 Ok(Some(receive))
502 }
503
504 async fn exit_lightning_receive(
510 &self,
511 lightning_receive: &LightningReceive,
512 ) -> anyhow::Result<()> {
513 ensure!(!lightning_receive.htlc_vtxos.is_empty(), "no HTLC VTXOs to exit");
514 let vtxos = lightning_receive.htlc_vtxos.iter().map(|v| &v.vtxo).collect::<Vec<_>>();
515
516 info!("Exiting HTLC VTXOs for lightning_receive with payment hash {}", lightning_receive.payment_hash);
517 self.inner.exit.start_exit_for_vtxos(&vtxos).await?;
518
519 if let Some(movement_id) = lightning_receive.movement_id {
520 self.inner.movements.finish_movement_with_update(
521 movement_id,
522 MovementStatus::Failed,
523 MovementUpdate::new().exited_vtxos(vtxos),
524 ).await?;
525 } else {
526 error!("movement id is missing but we disclosed preimage: {}", lightning_receive.payment_hash);
527 }
528
529 self.inner.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
530 Ok(())
531 }
532
533 pub(crate) async fn handle_failed_lightning_receive(
534 &self,
535 lightning_receive: &LightningReceive,
536 ) -> anyhow::Result<()> {
537 let vtxos = &lightning_receive.htlc_vtxos;
538
539 let update_opt = match (vtxos.is_empty(), lightning_receive.preimage_revealed_at) {
540 (false, Some(_)) => {
541 return Ok(());
546 }
547 (false, None) => {
548 warn!("HTLC-recv VTXOs are about to expire, but preimage has not been disclosed yet. Canceling");
549 self.mark_vtxos_as_spent(vtxos).await?;
550 if let Some(movement_id) = lightning_receive.movement_id {
551 Some((
552 movement_id,
553 MovementUpdate::new()
554 .effective_balance(SignedAmount::ZERO),
555 MovementStatus::Canceled,
556 ))
557 } else {
558 error!("movement id is missing but we got HTLC vtxos: {}", lightning_receive.payment_hash);
559 None
560 }
561 }
562 (true, Some(_)) => {
563 error!("No HTLC vtxos set on ln receive but preimage has been disclosed. Canceling");
564 lightning_receive.movement_id.map(|id| (id,
565 MovementUpdate::new()
566 .effective_balance(SignedAmount::ZERO),
567 MovementStatus::Canceled,
568 ))
569 }
570 (true, None) => None,
571 };
572
573 if let Some((movement_id, update, status)) = update_opt {
574 self.inner.movements.finish_movement_with_update(movement_id, status, update).await?;
575 }
576
577 self.inner.db.finish_pending_lightning_receive(lightning_receive.payment_hash).await?;
578
579 Ok(())
580 }
581
582 pub async fn cancel_lightning_receive(
591 &self,
592 payment_hash: PaymentHash,
593 ) -> anyhow::Result<()> {
594 let receive = self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
595 .context("no pending lightning receive found for this payment hash")?;
596
597 if receive.preimage_revealed_at.is_some() {
598 bail!("cannot cancel: preimage has already been revealed");
599 }
600
601 if receive.finished_at.is_some() {
602 bail!("lightning receive is already finished");
603 }
604
605 let (mut srv, _) = self.require_server().await?;
606 srv.client.cancel_lightning_receive(protos::CancelLightningReceiveRequest {
607 payment_hash: payment_hash.to_vec(),
608 }).await.context("server refused cancellation")?;
609
610 self.handle_failed_lightning_receive(&receive).await?;
612
613 Ok(())
614 }
615
616 pub async fn try_claim_lightning_receive(
642 &self,
643 payment_hash: PaymentHash,
644 wait: bool,
645 token: Option<&str>,
646 ) -> anyhow::Result<LightningReceive> {
647 trace!("Claiming lightning receive for payment hash: {}", payment_hash);
648
649 let key = format!("{}.{}", LIGHTNING_RECEIVE_LOCK_PREFIX, payment_hash);
653 let _guard = match self.inner.lock_manager.try_lock(&key).await {
654 Some(guard) => guard,
655 None => {
656 debug!("Receive operation already in progress for this payment");
657 return self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
658 .context("no receive for payment hash");
659 },
660 };
661
662 self.try_claim_lightning_receive_inner(payment_hash, wait, token).await
663 }
664
665 async fn try_claim_lightning_receive_inner(
667 &self,
668 payment_hash: PaymentHash,
669 wait: bool,
670 token: Option<&str>,
671 ) -> anyhow::Result<LightningReceive> {
672 let mut receive = match self.check_lightning_receive(payment_hash, wait, token).await? {
675 Some(receive) => receive,
676 None => {
677 return self.inner.db.fetch_lightning_receive_by_payment_hash(payment_hash).await?
678 .context("No receive for payment_hash")
679 }
680 };
681
682 if receive.finished_at.is_some() {
683 return Ok(receive);
684 }
685
686 if receive.htlc_vtxos.is_empty() {
689 return Ok(receive);
690 }
691
692 let mut retries_left = self.inner.config.lightning_receive_claim_retries;
693 let mut backoff = CLAIM_RETRY_BACKOFF_INITIAL;
694 let claim_result = loop {
695 match self.claim_lightning_receive(&mut receive).await {
696 Ok(()) => break Ok(()),
697 Err(e) if retries_left == 0 => break Err(e),
698 Err(e) => {
699 warn!(
700 "Error claiming lightning receive {} ({} retries left, retrying in {:?}): {:#}",
701 receive.payment_hash, retries_left, backoff, e,
702 );
703 retries_left -= 1;
704 tokio::time::sleep(backoff).await;
705 backoff = (backoff * 2).min(CLAIM_RETRY_BACKOFF_MAX);
706 }
707 }
708 };
709
710 match claim_result {
711 Ok(()) => Ok(receive),
712 Err(e) => {
713 error!("Failed to claim htlcs for payment_hash: {}", receive.payment_hash);
714 self.handle_failed_lightning_receive(&receive).await?;
717 Err(e)
718 }
719 }
720 }
721
722 pub async fn try_claim_all_lightning_receives(&self, wait: bool) -> anyhow::Result<Vec<LightningReceive>> {
738 let pending = self.pending_lightning_receives().await?;
739 let total = pending.len();
740
741 if total == 0 {
742 return Ok(vec![]);
743 }
744
745 let results: Vec<_> = tokio_stream::iter(pending)
746 .map(|rcv| async move {
747 self.try_claim_lightning_receive(rcv.invoice.into(), wait, None).await
748 })
749 .buffer_unordered(3)
750 .collect()
751 .await;
752
753 let mut claimed = vec![];
754 let mut failed = 0;
755
756 for result in results {
757 match result {
758 Ok(receive) => claimed.push(receive),
759 Err(e) => {
760 error!("Error claiming lightning receive: {:#}", e);
761 failed += 1;
762 }
763 }
764 }
765
766 if failed > 0 {
767 info!(
768 "Lightning receive claims: {} succeeded, {} failed out of {} pending",
769 claimed.len(), failed, total
770 );
771 }
772
773 if claimed.is_empty() {
774 anyhow::bail!("All {} lightning receive claim(s) failed", failed);
775 }
776
777 Ok(claimed)
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784
785 const TEST_INVOICE_STR: &str = "lntbs100u1p5j0x82sp5d0rwfh7tgrrlwsegy9rx3tzpt36cqwjqza5x4wvcjxjzscfaf6jspp5d8q7354dg3p8h0kywhqq5dq984r8f5en98hf9ln85ug0w8fx6hhsdqqcqzpc9qyysgqyk54v7tpzprxll7e0jyvtxcpgwttzk84wqsfjsqvcdtq47zt2wssxsmtjhz8dka62mdnf9jafhu3l4cpyfnsx449v4wstrwzzql2w5qqs8uh7p";
786
787 fn test_bolt11() -> Bolt11Invoice {
788 Bolt11Invoice::from_str(TEST_INVOICE_STR).expect("valid test invoice")
789 }
790
791 #[test]
792 fn validate_bolt11_payment_hash_accepts_matching_hash() {
793 let invoice = test_bolt11();
794 let payment_hash = PaymentHash::from(&invoice);
795
796 validate_bolt11_payment_hash(&invoice, payment_hash).unwrap();
797 }
798
799 #[test]
800 fn validate_bolt11_payment_hash_rejects_mismatched_hash() {
801 let invoice = test_bolt11();
802 let mismatched_payment_hash = PaymentHash::from_slice(&[0xabu8; 32]).unwrap();
803
804 let err = validate_bolt11_payment_hash(&invoice, mismatched_payment_hash)
805 .expect_err("mismatched payment hash should fail");
806
807 assert!(
808 err.to_string().contains("returned invoice with payment hash"),
809 "{err:?}",
810 );
811 }
812}