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