1use anyhow::ensure;
2use bitcoin::hashes::sha256;
3use clap::{Parser, Subcommand};
4use devimint::devfed::DevJitFed;
5use devimint::federation::{Client, Federation};
6use devimint::util::{ProcessManager, almost_equal};
7use devimint::version_constants::{
8 VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA, VERSION_0_12_0_ALPHA,
9};
10use devimint::{Gatewayd, cmd, util};
11use fedimint_core::core::OperationId;
12use fedimint_core::encoding::Encodable;
13use fedimint_core::task::{self};
14use fedimint_core::util::{backoff_util, retry};
15use fedimint_lnurl::{LnurlResponse, VerifyResponse, parse_lnurl};
16use fedimint_lnv2_client::FinalSendOperationState;
17use lightning_invoice::Bolt11Invoice;
18use serde::Deserialize;
19use tokio::try_join;
20use tracing::info;
21
22#[path = "common.rs"]
23mod common;
24
25async fn module_is_present(client: &Client, kind: &str) -> anyhow::Result<bool> {
26 let modules = cmd!(client, "module").out_json().await?;
27
28 let modules = modules["list"].as_array().expect("module list is an array");
29
30 Ok(modules.iter().any(|m| m["kind"].as_str() == Some(kind)))
31}
32
33async fn assert_module_sanity(client: &Client) -> anyhow::Result<()> {
34 if !devimint::util::is_backwards_compatibility_test() {
35 ensure!(
36 !module_is_present(client, "ln").await?,
37 "ln module should not be present"
38 );
39 }
40
41 Ok(())
42}
43
44#[derive(Parser)]
45#[command(name = "lnv2-module-tests")]
46#[command(about = "LNv2 module integration tests", long_about = None)]
47struct Cli {
48 #[command(subcommand)]
49 command: Option<Commands>,
50}
51
52#[derive(Subcommand)]
53enum Commands {
54 GatewayRegistration,
56 Payments,
58 LnurlPay,
60 LnurlRecovery,
62 DuplicatePayment,
64}
65
66#[tokio::main]
67async fn main() -> anyhow::Result<()> {
68 let cli = Cli::parse();
69
70 let num_feds = if matches!(cli.command, None | Some(Commands::DuplicatePayment)) {
73 2
74 } else {
75 1
76 };
77
78 devimint::run_devfed_test()
79 .num_feds(num_feds)
80 .call(|dev_fed, process_mgr| async move {
81 if !devimint::util::supports_lnv2() {
82 info!("lnv2 is disabled, skipping");
83 return Ok(());
84 }
85
86 match &cli.command {
87 Some(Commands::GatewayRegistration) => {
88 test_gateway_registration(&dev_fed).await?;
89 }
90 Some(Commands::Payments) => {
91 test_payments(&dev_fed).await?;
92 }
93 Some(Commands::LnurlPay) => {
94 pegin_gateways(&dev_fed).await?;
95 test_lnurl_pay(&dev_fed).await?;
96 }
97 Some(Commands::LnurlRecovery) => {
98 pegin_gateways(&dev_fed).await?;
99 test_lnurl_recovery(&dev_fed).await?;
100 }
101 Some(Commands::DuplicatePayment) => {
102 pegin_gateways(&dev_fed).await?;
103 test_duplicate_payment(&dev_fed, &process_mgr).await?;
104 }
105 None => {
106 test_gateway_registration(&dev_fed).await?;
108 test_payments(&dev_fed).await?;
109 test_duplicate_payment(&dev_fed, &process_mgr).await?;
110 test_lnurl_pay(&dev_fed).await?;
111 test_lnurl_recovery(&dev_fed).await?;
112 }
113 }
114
115 info!("Testing LNV2 is complete!");
116
117 Ok(())
118 })
119 .await
120}
121
122async fn pegin_gateways(dev_fed: &DevJitFed) -> anyhow::Result<()> {
123 info!("Pegging-in gateways...");
124
125 let federation = dev_fed.fed().await?;
126
127 let gw_lnd = dev_fed.gw_lnd().await?;
128 let gw_ldk = dev_fed.gw_ldk().await?;
129
130 federation
131 .pegin_gateways(1_000_000, vec![gw_lnd, gw_ldk])
132 .await?;
133
134 Ok(())
135}
136
137async fn test_gateway_registration(dev_fed: &DevJitFed) -> anyhow::Result<()> {
138 let client = dev_fed
139 .fed()
140 .await?
141 .new_joined_client("lnv2-test-gateway-registration-client")
142 .await?;
143
144 assert_module_sanity(&client).await?;
145
146 let gw_lnd = dev_fed.gw_lnd().await?;
147 let gw_ldk = dev_fed.gw_ldk_connected().await?;
148
149 let gateways = [gw_lnd.addr.clone(), gw_ldk.addr.clone()];
150
151 info!("Testing registration of gateways...");
152
153 for gateway in &gateways {
154 for peer in 0..dev_fed.fed().await?.members.len() {
155 assert!(add_gateway(&client, peer, gateway).await?);
156 }
157 }
158
159 assert_eq!(
160 cmd!(client, "module", "lnv2", "gateways", "list")
161 .out_json()
162 .await?
163 .as_array()
164 .expect("JSON Value is not an array")
165 .len(),
166 2
167 );
168
169 assert_eq!(
170 cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
171 .out_json()
172 .await?
173 .as_array()
174 .expect("JSON Value is not an array")
175 .len(),
176 2
177 );
178
179 info!("Testing selection of gateways...");
180
181 assert!(
182 gateways.contains(
183 &cmd!(client, "module", "lnv2", "gateways", "select")
184 .out_json()
185 .await?
186 .as_str()
187 .expect("JSON Value is not a string")
188 .to_string()
189 )
190 );
191
192 cmd!(client, "module", "lnv2", "gateways", "map")
193 .out_json()
194 .await?;
195
196 for _ in 0..10 {
197 for gateway in &gateways {
198 let invoice = common::receive(&client, gateway, 1_000_000).await?.0;
199
200 assert_eq!(
201 cmd!(
202 client,
203 "module",
204 "lnv2",
205 "gateways",
206 "select",
207 "--invoice",
208 invoice.to_string()
209 )
210 .out_json()
211 .await?
212 .as_str()
213 .expect("JSON Value is not a string"),
214 gateway
215 )
216 }
217 }
218
219 info!("Testing deregistration of gateways...");
220
221 for gateway in &gateways {
222 for peer in 0..dev_fed.fed().await?.members.len() {
223 assert!(remove_gateway(&client, peer, gateway).await?);
224 }
225 }
226
227 assert!(
228 cmd!(client, "module", "lnv2", "gateways", "list")
229 .out_json()
230 .await?
231 .as_array()
232 .expect("JSON Value is not an array")
233 .is_empty(),
234 );
235
236 assert!(
237 cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
238 .out_json()
239 .await?
240 .as_array()
241 .expect("JSON Value is not an array")
242 .is_empty()
243 );
244
245 Ok(())
246}
247
248async fn test_payments(dev_fed: &DevJitFed) -> anyhow::Result<()> {
249 let federation = dev_fed.fed().await?;
250
251 let client = federation
252 .new_joined_client("lnv2-test-payments-client")
253 .await?;
254
255 assert_module_sanity(&client).await?;
256
257 federation.pegin_client(10_000, &client).await?;
258
259 almost_equal(client.balance().await?, 10_000 * 1000, 500_000).unwrap();
260
261 let gw_lnd = dev_fed.gw_lnd().await?;
262 let gw_ldk = dev_fed.gw_ldk().await?;
263 let lnd = dev_fed.lnd().await?;
264
265 let (hold_preimage, hold_invoice, hold_payment_hash) = lnd.create_hold_invoice(60000).await?;
266
267 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
268
269 let gateway_matrix = [
270 (gw_lnd, gw_lnd),
271 (gw_lnd, gw_ldk),
272 (gw_ldk, gw_lnd),
273 (gw_ldk, gw_ldk),
274 ];
275
276 info!("Testing refund of circular payments...");
277
278 for (gw_send, gw_receive) in gateway_matrix {
279 info!(
280 "Testing refund of payment: client -> {} -> {} -> client",
281 gw_send.ln.ln_type(),
282 gw_receive.ln.ln_type()
283 );
284
285 let invoice = common::receive(&client, &gw_receive.addr, 1_000_000)
286 .await?
287 .0;
288
289 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
290 assert!(matches!(state, FinalSendOperationState::Refunded));
291 }
292
293 pegin_gateways(dev_fed).await?;
294
295 info!("Testing circular payments...");
296
297 for (gw_send, gw_receive) in gateway_matrix {
298 info!(
299 "Testing payment: client -> {} -> {} -> client",
300 gw_send.ln.ln_type(),
301 gw_receive.ln.ln_type()
302 );
303
304 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
305
306 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
307 assert!(matches!(state, FinalSendOperationState::Success(_)));
308
309 common::await_receive_claimed(&client, receive_op).await?;
310 }
311
312 info!("Testing payments from client to gateways...");
313
314 for (gw_send, gw_receive) in gateway_pairs {
315 info!(
316 "Testing payment: client -> {} -> {}",
317 gw_send.ln.ln_type(),
318 gw_receive.ln.ln_type()
319 );
320
321 let invoice = gw_receive.client().create_invoice(1_000_000).await?;
322
323 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
324 assert!(matches!(state, FinalSendOperationState::Success(_)));
325 }
326
327 info!("Testing payments from gateways to client...");
328
329 for (gw_send, gw_receive) in gateway_pairs {
330 info!(
331 "Testing payment: {} -> {} -> client",
332 gw_send.ln.ln_type(),
333 gw_receive.ln.ln_type()
334 );
335
336 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
337
338 gw_send.client().pay_invoice(invoice).await?;
339
340 common::await_receive_claimed(&client, receive_op).await?;
341 }
342
343 retry(
344 "Waiting for the full balance to become available to the client".to_string(),
345 backoff_util::background_backoff(),
346 || async {
347 ensure!(client.balance().await? >= 9000 * 1000);
348
349 Ok(())
350 },
351 )
352 .await?;
353
354 info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
355
356 let (state, _) = try_join!(
357 common::send(&client, &gw_ldk.addr, &hold_invoice),
358 lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
359 )?;
360 assert!(matches!(state, FinalSendOperationState::Success(_)));
361
362 info!("Testing LNv2 lightning fees...");
363
364 let fed_id = federation.calculate_federation_id();
365
366 gw_lnd
367 .client()
368 .set_federation_routing_fee(fed_id.clone(), 0, 0)
369 .await?;
370
371 gw_lnd
372 .client()
373 .set_federation_transaction_fee(fed_id.clone(), 0, 0)
374 .await?;
375
376 test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
379
380 let online_peers: Vec<usize> = federation.members.keys().copied().collect();
381
382 test_iroh_payment(&client, gw_lnd, gw_ldk, &online_peers).await?;
383
384 info!("Testing payment summary...");
385
386 let lnd_payment_summary = gw_lnd.client().payment_summary().await?;
387
388 assert_eq!(lnd_payment_summary.outgoing.total_success, 5);
389 assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
390 assert_eq!(lnd_payment_summary.incoming.total_success, 4);
391 assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
392
393 assert!(lnd_payment_summary.outgoing.median_latency.is_some());
394 assert!(lnd_payment_summary.outgoing.average_latency.is_some());
395 assert!(lnd_payment_summary.incoming.median_latency.is_some());
396 assert!(lnd_payment_summary.incoming.average_latency.is_some());
397
398 let ldk_payment_summary = gw_ldk.client().payment_summary().await?;
399
400 assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
401 assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
402 assert_eq!(ldk_payment_summary.incoming.total_success, 4);
403 assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
404
405 assert!(ldk_payment_summary.outgoing.median_latency.is_some());
406 assert!(ldk_payment_summary.outgoing.average_latency.is_some());
407 assert!(ldk_payment_summary.incoming.median_latency.is_some());
408 assert!(ldk_payment_summary.incoming.average_latency.is_some());
409
410 Ok(())
411}
412
413async fn join_client(name: &str, invite_code: &str) -> anyhow::Result<Client> {
417 let client = Client::create(name).await?;
418 client.join_federation(invite_code.to_string()).await?;
419 Ok(client)
420}
421
422async fn test_duplicate_payment(
428 dev_fed: &DevJitFed,
429 process_mgr: &ProcessManager,
430) -> anyhow::Result<()> {
431 info!(
432 "Testing three clients across two federations paying the same invoice settle exactly once..."
433 );
434
435 let gw_send = dev_fed.gw_ldk().await?;
438 let gw_receive = dev_fed.gw_lnd().await?;
439
440 if gw_send.gatewayd_version < *VERSION_0_12_0_ALPHA {
443 info!(
444 gatewayd_version = %gw_send.gatewayd_version,
445 "Skipping: gateway predates cross-federation outgoing payment dedup"
446 );
447 return Ok(());
448 }
449
450 let first_federation = dev_fed.fed().await?;
451
452 let first_invite = first_federation.invite_code()?;
457
458 let second_federation = Federation::new(
462 process_mgr,
463 dev_fed.bitcoind().await?.clone(),
464 false,
465 false,
466 false,
467 1,
468 "lnv2-duplicate-payment".to_string(),
469 )
470 .await?;
471 let second_invite = second_federation.invite_code()?;
472
473 assert_ne!(
474 first_federation.calculate_federation_id(),
475 second_federation.calculate_federation_id(),
476 "the second federation must be distinct from the first"
477 );
478
479 gw_send.client().connect_fed(second_invite.clone()).await?;
480 second_federation
481 .pegin_gateways(1_000_000, vec![gw_send])
482 .await?;
483
484 let client_a = join_client("lnv2-duplicate-payment-a", &first_invite).await?;
485 let client_b = join_client("lnv2-duplicate-payment-b", &first_invite).await?;
486 let client_c = join_client("lnv2-duplicate-payment-c", &second_invite).await?;
487
488 first_federation.pegin_client(10_000, &client_a).await?;
489 first_federation.pegin_client(10_000, &client_b).await?;
490 second_federation.pegin_client(10_000, &client_c).await?;
491
492 let control_invoice = gw_receive
497 .client()
498 .create_invoice(1_000_000)
499 .await?
500 .to_string();
501 let control = common::send(&client_c, &gw_send.addr, &control_invoice).await?;
502 assert!(
503 matches!(control, FinalSendOperationState::Success(_)),
504 "second-federation control payment must succeed, got {control:?}"
505 );
506
507 let invoice = gw_receive
508 .client()
509 .create_invoice(1_000_000)
510 .await?
511 .to_string();
512
513 let (state_a, state_b) = try_join!(
516 common::send(&client_a, &gw_send.addr, &invoice),
517 common::send(&client_b, &gw_send.addr, &invoice),
518 )?;
519
520 let state_c = common::send(&client_c, &gw_send.addr, &invoice).await?;
524
525 let states = [state_a, state_b, state_c];
526
527 info!("shared-invoice send states: {states:?}");
528
529 let successes = states
530 .iter()
531 .filter(|state| matches!(state, FinalSendOperationState::Success(_)))
532 .count();
533
534 let refunds = states
535 .iter()
536 .filter(|state| matches!(state, FinalSendOperationState::Refunded))
537 .count();
538
539 assert_eq!(
540 (successes, refunds),
541 (1, 2),
542 "exactly one payment must settle and the other two be refunded, got {states:?}"
543 );
544
545 Ok(())
546}
547
548async fn test_fees(
549 fed_id: String,
550 client: &Client,
551 gw_lnd: &Gatewayd,
552 gw_ldk: &Gatewayd,
553 expected_addition: u64,
554) -> anyhow::Result<()> {
555 let gw_lnd_ecash_prev = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
556
557 let (invoice, receive_op) = common::receive(client, &gw_ldk.addr, 1_000_000).await?;
558
559 let state = common::send(client, &gw_lnd.addr, &invoice.to_string()).await?;
560 assert!(matches!(state, FinalSendOperationState::Success(_)));
561
562 common::await_receive_claimed(client, receive_op).await?;
563
564 while almost_equal(
568 gw_lnd_ecash_prev + expected_addition,
569 gw_lnd.client().ecash_balance(fed_id.clone()).await?,
570 5000,
571 )
572 .is_err()
573 {
574 info!("Waiting for the sending gateway's outgoing claim to settle...");
575 cmd!(client, "dev", "wait", "1").out_json().await?;
576 }
577
578 Ok(())
579}
580
581async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
582 cmd!(
583 client,
584 "--our-id",
585 peer.to_string(),
586 "--password",
587 "pass",
588 "module",
589 "lnv2",
590 "gateways",
591 "add",
592 gateway
593 )
594 .out_json()
595 .await?
596 .as_bool()
597 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
598}
599
600async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
601 cmd!(
602 client,
603 "--our-id",
604 peer.to_string(),
605 "--password",
606 "pass",
607 "module",
608 "lnv2",
609 "gateways",
610 "remove",
611 gateway
612 )
613 .out_json()
614 .await?
615 .as_bool()
616 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
617}
618
619const LNURL_BALANCE_WAIT_ATTEMPTS: u64 = 120;
622
623async fn wait_for_lnurl_balance(
624 client: &Client,
625 client_name: &str,
626 expected_msats: u64,
627) -> anyhow::Result<()> {
628 let mut last_balance_msats = client.balance().await?;
629
630 for attempt in 1..=LNURL_BALANCE_WAIT_ATTEMPTS {
631 if last_balance_msats < expected_msats {
632 info!(
633 client_name,
634 balance_msats = last_balance_msats,
635 expected_msats,
636 attempt,
637 "Waiting for {client_name} to receive funds via LNURL"
638 );
639 cmd!(client, "dev", "wait", "1").out_json().await?;
640 last_balance_msats = client.balance().await?;
641 } else {
642 info!(
643 client_name,
644 balance_msats = last_balance_msats,
645 expected_msats,
646 attempt,
647 "{client_name} successfully received funds via LNURL"
648 );
649 return Ok(());
650 }
651 }
652
653 if last_balance_msats < expected_msats {
654 anyhow::bail!(
655 "timed out waiting for {client_name} to receive funds via LNURL after {} attempts; last balance: {last_balance_msats} msats; expected balance: {expected_msats} msats",
656 LNURL_BALANCE_WAIT_ATTEMPTS
657 );
658 }
659
660 info!(
661 client_name,
662 balance_msats = last_balance_msats,
663 expected_msats,
664 attempt = LNURL_BALANCE_WAIT_ATTEMPTS,
665 "{client_name} successfully received funds via LNURL"
666 );
667
668 Ok(())
669}
670
671async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
672 if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
673 return Ok(());
674 }
675
676 if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
677 return Ok(());
678 }
679
680 if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
681 return Ok(());
682 }
683
684 let federation = dev_fed.fed().await?;
685
686 let gw_lnd = dev_fed.gw_lnd().await?;
687 let gw_ldk = dev_fed.gw_ldk().await?;
688
689 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
690
691 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
692
693 let client_a = federation
694 .new_joined_client("lnv2-lnurl-test-client-a")
695 .await?;
696
697 assert_module_sanity(&client_a).await?;
698
699 let client_b = federation
700 .new_joined_client("lnv2-lnurl-test-client-b")
701 .await?;
702
703 assert_module_sanity(&client_b).await?;
704
705 for (gw_send, gw_receive) in gateway_pairs {
706 info!(
707 "Testing lnurl payments: {} -> {} -> client",
708 gw_send.ln.ln_type(),
709 gw_receive.ln.ln_type()
710 );
711
712 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
713 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
714
715 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
716 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
717
718 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
719 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
720
721 let response_a = verify_payment(&verify_url_a).await?;
722 let response_b = verify_payment(&verify_url_b).await?;
723
724 assert!(!response_a.settled);
725 assert!(!response_b.settled);
726
727 assert!(response_a.preimage.is_none());
728 assert!(response_b.preimage.is_none());
729
730 gw_send.client().pay_invoice(invoice_a.clone()).await?;
731 gw_send.client().pay_invoice(invoice_b.clone()).await?;
732
733 let response_a = verify_payment(&verify_url_a).await?;
734 let response_b = verify_payment(&verify_url_b).await?;
735
736 assert!(response_a.settled);
737 assert!(response_b.settled);
738
739 verify_preimage(&response_a, &invoice_a);
740 verify_preimage(&response_b, &invoice_b);
741
742 assert_eq!(verify_task_a.await??, response_a);
743 assert_eq!(verify_task_b.await??, response_b);
744 }
745
746 wait_for_lnurl_balance(&client_a, "client A", 950 * 1000).await?;
747 wait_for_lnurl_balance(&client_b, "client B", 950 * 1000).await?;
748
749 Ok(())
750}
751
752async fn test_lnurl_recovery(dev_fed: &DevJitFed) -> anyhow::Result<()> {
760 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA {
764 return Ok(());
765 }
766
767 let federation = dev_fed.fed().await?;
768 let gw_lnd = dev_fed.gw_lnd().await?;
769 let gw_ldk = dev_fed.gw_ldk().await?;
770 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
771
772 const LNURL_AMOUNT_MSAT: u64 = 500_000;
773 const LNURL_BALANCE_TOLERANCE_MSAT: u64 = 100_000;
774
775 info!("Phase 1: Creating client and receiving via LNURL before recovery");
778
779 let client = federation
780 .new_joined_client("lnv2-lnurl-recovery-original")
781 .await?;
782
783 let lnurl = generate_lnurl(&client, &recurringd, &gw_ldk.addr).await?;
784
785 for i in 0..3 {
786 info!("Paying LNURL invoice {}/3", i + 1);
787 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
788 gw_lnd.client().pay_invoice(invoice).await?;
789 }
790
791 while almost_equal(
792 client.balance().await?,
793 3 * LNURL_AMOUNT_MSAT,
794 LNURL_BALANCE_TOLERANCE_MSAT,
795 )
796 .is_err()
797 {
798 info!("Waiting for pre-recovery LNURL payments to settle...");
799 cmd!(client, "dev", "wait", "1").out_json().await?;
800 }
801
802 let pre_recovery_balance = client.balance().await?;
803 info!("Pre-recovery balance: {pre_recovery_balance} msats");
804
805 let mnemonic = cmd!(client, "print-secret").out_json().await?["secret"]
806 .as_str()
807 .expect("secret is a string")
808 .to_owned();
809
810 info!("Phase 2: Recovering client from seed");
813
814 let restored = Client::create("lnv2-lnurl-recovery-restored").await?;
815 cmd!(
816 restored,
817 "restore",
818 "--invite-code",
819 federation.invite_code()?,
820 "--mnemonic",
821 &mnemonic
822 )
823 .run()
824 .await?;
825
826 while restored.balance().await? < pre_recovery_balance {
827 info!("Waiting for recovery to complete...");
828 cmd!(restored, "dev", "wait", "1").out_json().await?;
829 }
830
831 let post_recovery_balance = restored.balance().await?;
832 info!("Post-recovery balance: {post_recovery_balance} msats");
833
834 info!("Phase 3: Paying to the original LNURL; restored client must claim");
837
838 for i in 0..2 {
841 info!("Paying pre-recovery LNURL invoice {}/2", i + 1);
842 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
843 gw_lnd.client().pay_invoice(invoice).await?;
844 }
845
846 while almost_equal(
847 restored.balance().await?,
848 post_recovery_balance + 2 * LNURL_AMOUNT_MSAT,
849 LNURL_BALANCE_TOLERANCE_MSAT,
850 )
851 .is_err()
852 {
853 info!("Waiting for post-recovery LNURL payments to settle...");
854 cmd!(restored, "dev", "wait", "1").out_json().await?;
855 }
856
857 let final_balance = restored.balance().await?;
858 info!("Final balance: {final_balance} msats");
859
860 let operations = cmd!(restored, "list-operations", "--limit", "100")
861 .out_json()
862 .await?;
863 let lnv2_ops: Vec<_> = operations["operations"]
864 .as_array()
865 .expect("operations is an array")
866 .iter()
867 .filter(|op| op["operation_kind"].as_str() == Some("lnv2"))
868 .collect();
869 assert!(
870 lnv2_ops.len() >= 2,
871 "Expected at least 2 LNv2 operations after post-recovery receives, found {}",
872 lnv2_ops.len()
873 );
874
875 info!(
876 "LNURL recovery test passed: {} new operations, balance {final_balance} msats",
877 lnv2_ops.len()
878 );
879
880 Ok(())
881}
882
883async fn generate_lnurl(
884 client: &Client,
885 recurringd_base_url: &str,
886 gw_ldk_addr: &str,
887) -> anyhow::Result<String> {
888 cmd!(
889 client,
890 "module",
891 "lnv2",
892 "lnurl",
893 "generate",
894 recurringd_base_url,
895 "--gateway",
896 gw_ldk_addr,
897 )
898 .out_json()
899 .await
900 .map(|s| s.as_str().unwrap().to_owned())
901}
902
903fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
904 let preimage = response.preimage.expect("Payment should be settled");
905
906 let payment_hash = preimage.consensus_hash::<sha256::Hash>();
907
908 assert_eq!(payment_hash, *invoice.payment_hash());
909}
910
911async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
912 reqwest::get(verify_url)
913 .await?
914 .json::<LnurlResponse<VerifyResponse>>()
915 .await?
916 .into_result()
917 .map_err(anyhow::Error::msg)
918}
919
920async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
921 reqwest::get(format!("{verify_url}?wait"))
922 .await?
923 .json::<LnurlResponse<VerifyResponse>>()
924 .await?
925 .into_result()
926 .map_err(anyhow::Error::msg)
927}
928
929#[derive(Deserialize, Clone)]
930struct LnUrlPayResponse {
931 callback: String,
932}
933
934#[derive(Deserialize, Clone)]
935struct LnUrlPayInvoiceResponse {
936 pr: Bolt11Invoice,
937 verify: String,
938}
939
940async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
941 let url = parse_lnurl(&lnurl).ok_or_else(|| anyhow::anyhow!("Invalid LNURL"))?;
942
943 let response = reqwest::get(url).await?.json::<LnUrlPayResponse>().await?;
944
945 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
946
947 let response = reqwest::get(callback_url)
948 .await?
949 .json::<LnUrlPayInvoiceResponse>()
950 .await?;
951
952 ensure!(
953 response.pr.amount_milli_satoshis() == Some(amount_msat),
954 "Invoice amount is not set"
955 );
956
957 Ok((response.pr, response.verify))
958}
959
960async fn test_iroh_payment(
961 client: &Client,
962 gw_lnd: &Gatewayd,
963 gw_ldk: &Gatewayd,
964 online_peers: &[usize],
965) -> anyhow::Result<()> {
966 info!("Testing iroh payment...");
967 for &peer in online_peers {
968 add_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
969 }
970
971 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
974 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
975 {
976 for &peer in online_peers {
977 add_gateway(client, peer, &gw_lnd.addr).await?;
978 }
979 }
980
981 let invoice = gw_ldk.client().create_invoice(5_000_000).await?;
982
983 let send_op = serde_json::from_value::<OperationId>(
984 cmd!(client, "module", "lnv2", "send", invoice,)
985 .out_json()
986 .await?,
987 )?;
988
989 let send_state = common::await_send(client, send_op).await?;
990 assert!(
991 matches!(send_state, FinalSendOperationState::Success(_)),
992 "unexpected send state: {send_state:?}"
993 );
994
995 let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
996 cmd!(client, "module", "lnv2", "receive", "5000000",)
997 .out_json()
998 .await?,
999 )?;
1000
1001 gw_ldk.client().pay_invoice(invoice).await?;
1002 common::await_receive_claimed(client, receive_op).await?;
1003
1004 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1005 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1006 {
1007 for &peer in online_peers {
1008 remove_gateway(client, peer, &gw_lnd.addr).await?;
1009 }
1010 }
1011
1012 for &peer in online_peers {
1013 remove_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1014 }
1015
1016 Ok(())
1017}