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 (hold_preimage, hold_invoice, hold_payment_hash) = lnd.create_hold_invoice(60000).await?;
274
275 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
276
277 let gateway_matrix = [
278 (gw_lnd, gw_lnd),
279 (gw_lnd, gw_ldk),
280 (gw_ldk, gw_lnd),
281 (gw_ldk, gw_ldk),
282 ];
283
284 info!("Testing refund of circular payments...");
285
286 for (gw_send, gw_receive) in gateway_matrix {
287 info!(
288 "Testing refund of payment: client -> {} -> {} -> client",
289 gw_send.ln.ln_type(),
290 gw_receive.ln.ln_type()
291 );
292
293 let invoice = common::receive(&client, &gw_receive.addr, 1_000_000)
294 .await?
295 .0;
296
297 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
298 assert!(matches!(state, FinalSendOperationState::Refunded));
299 }
300
301 pegin_gateways(dev_fed).await?;
302
303 info!("Testing circular payments...");
304
305 for (gw_send, gw_receive) in gateway_matrix {
306 info!(
307 "Testing payment: client -> {} -> {} -> client",
308 gw_send.ln.ln_type(),
309 gw_receive.ln.ln_type()
310 );
311
312 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
313
314 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
315 assert!(matches!(state, FinalSendOperationState::Success(_)));
316
317 common::await_receive_claimed(&client, receive_op).await?;
318 }
319
320 info!("Testing payments from client to gateways...");
321
322 for (gw_send, gw_receive) in gateway_pairs {
323 info!(
324 "Testing payment: client -> {} -> {}",
325 gw_send.ln.ln_type(),
326 gw_receive.ln.ln_type()
327 );
328
329 let invoice = gw_receive.client().create_invoice(1_000_000).await?;
330
331 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
332 assert!(matches!(state, FinalSendOperationState::Success(_)));
333 }
334
335 info!("Testing payments from gateways to client...");
336
337 for (gw_send, gw_receive) in gateway_pairs {
338 info!(
339 "Testing payment: {} -> {} -> client",
340 gw_send.ln.ln_type(),
341 gw_receive.ln.ln_type()
342 );
343
344 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
345
346 gw_send.client().pay_invoice(invoice).await?;
347
348 common::await_receive_claimed(&client, receive_op).await?;
349 }
350
351 retry(
352 "Waiting for the full balance to become available to the client".to_string(),
353 backoff_util::background_backoff(),
354 || async {
355 ensure!(client.balance().await? >= 9000 * 1000);
356
357 Ok(())
358 },
359 )
360 .await?;
361
362 info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
363
364 let (state, _) = try_join!(
365 common::send(&client, &gw_ldk.addr, &hold_invoice),
366 lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
367 )?;
368 assert!(matches!(state, FinalSendOperationState::Success(_)));
369
370 info!("Testing LNv2 lightning fees...");
371
372 let fed_id = federation.calculate_federation_id();
373
374 gw_lnd
375 .client()
376 .set_federation_routing_fee(fed_id.clone(), 0, 0)
377 .await?;
378
379 gw_lnd
380 .client()
381 .set_federation_transaction_fee(fed_id.clone(), 0, 0)
382 .await?;
383
384 test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
387
388 let online_peers: Vec<usize> = federation.members.keys().copied().collect();
389
390 test_iroh_payment(&client, gw_lnd, gw_ldk, &online_peers).await?;
391
392 info!("Testing payment summary...");
393
394 let lnd_payment_summary = gw_lnd.client().payment_summary().await?;
395
396 assert_eq!(lnd_payment_summary.outgoing.total_success, 5);
397 assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
398 assert_eq!(lnd_payment_summary.incoming.total_success, 4);
399 assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
400
401 assert!(lnd_payment_summary.outgoing.median_latency.is_some());
402 assert!(lnd_payment_summary.outgoing.average_latency.is_some());
403 assert!(lnd_payment_summary.incoming.median_latency.is_some());
404 assert!(lnd_payment_summary.incoming.average_latency.is_some());
405
406 let ldk_payment_summary = gw_ldk.client().payment_summary().await?;
407
408 assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
409 assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
410 assert_eq!(ldk_payment_summary.incoming.total_success, 4);
411 assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
412
413 assert!(ldk_payment_summary.outgoing.median_latency.is_some());
414 assert!(ldk_payment_summary.outgoing.average_latency.is_some());
415 assert!(ldk_payment_summary.incoming.median_latency.is_some());
416 assert!(ldk_payment_summary.incoming.average_latency.is_some());
417
418 Ok(())
419}
420
421async fn join_client(name: &str, invite_code: &str) -> anyhow::Result<Client> {
425 let client = Client::create(name).await?;
426 client.join_federation(invite_code.to_string()).await?;
427 Ok(client)
428}
429
430async fn test_duplicate_payment(
436 dev_fed: &DevJitFed,
437 process_mgr: &ProcessManager,
438) -> anyhow::Result<()> {
439 info!(
440 "Testing three clients across two federations paying the same invoice settle exactly once..."
441 );
442
443 let gw_send = dev_fed.gw_ldk().await?;
446 let gw_receive = dev_fed.gw_lnd().await?;
447
448 if gw_send.gatewayd_version < *VERSION_0_12_0_ALPHA {
451 info!(
452 gatewayd_version = %gw_send.gatewayd_version,
453 "Skipping: gateway predates cross-federation outgoing payment dedup"
454 );
455 return Ok(());
456 }
457
458 let first_federation = dev_fed.fed().await?;
459
460 let first_invite = first_federation.invite_code()?;
465
466 let second_federation = Federation::new(
470 process_mgr,
471 dev_fed.bitcoind().await?.clone(),
472 false,
473 false,
474 false,
475 1,
476 "lnv2-duplicate-payment".to_string(),
477 )
478 .await?;
479 let second_invite = second_federation.invite_code()?;
480
481 assert_ne!(
482 first_federation.calculate_federation_id(),
483 second_federation.calculate_federation_id(),
484 "the second federation must be distinct from the first"
485 );
486
487 let client_dir: PathBuf = std::env::var(FM_CLIENT_DIR_ENV)?.parse()?;
493 write_overwrite_async(client_dir.join("invite-code"), &first_invite).await?;
494
495 gw_send.client().connect_fed(second_invite.clone()).await?;
496 second_federation
497 .pegin_gateways(1_000_000, vec![gw_send])
498 .await?;
499
500 let client_a = join_client("lnv2-duplicate-payment-a", &first_invite).await?;
501 let client_b = join_client("lnv2-duplicate-payment-b", &first_invite).await?;
502 let client_c = join_client("lnv2-duplicate-payment-c", &second_invite).await?;
503
504 first_federation.pegin_client(10_000, &client_a).await?;
505 first_federation.pegin_client(10_000, &client_b).await?;
506 second_federation.pegin_client(10_000, &client_c).await?;
507
508 let control_invoice = gw_receive
513 .client()
514 .create_invoice(1_000_000)
515 .await?
516 .to_string();
517 let control = common::send(&client_c, &gw_send.addr, &control_invoice).await?;
518 assert!(
519 matches!(control, FinalSendOperationState::Success(_)),
520 "second-federation control payment must succeed, got {control:?}"
521 );
522
523 let invoice = gw_receive
524 .client()
525 .create_invoice(1_000_000)
526 .await?
527 .to_string();
528
529 let (state_a, state_b) = try_join!(
532 common::send(&client_a, &gw_send.addr, &invoice),
533 common::send(&client_b, &gw_send.addr, &invoice),
534 )?;
535
536 let state_c = common::send(&client_c, &gw_send.addr, &invoice).await?;
540
541 let states = [state_a, state_b, state_c];
542
543 info!("shared-invoice send states: {states:?}");
544
545 let successes = states
546 .iter()
547 .filter(|state| matches!(state, FinalSendOperationState::Success(_)))
548 .count();
549
550 let refunds = states
551 .iter()
552 .filter(|state| matches!(state, FinalSendOperationState::Refunded))
553 .count();
554
555 assert_eq!(
556 (successes, refunds),
557 (1, 2),
558 "exactly one payment must settle and the other two be refunded, got {states:?}"
559 );
560
561 Ok(())
562}
563
564async fn test_fees(
565 fed_id: String,
566 client: &Client,
567 gw_lnd: &Gatewayd,
568 gw_ldk: &Gatewayd,
569 expected_addition: u64,
570) -> anyhow::Result<()> {
571 let gw_lnd_ecash_prev = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
572
573 let (invoice, receive_op) = common::receive(client, &gw_ldk.addr, 1_000_000).await?;
574
575 let state = common::send(client, &gw_lnd.addr, &invoice.to_string()).await?;
576 assert!(matches!(state, FinalSendOperationState::Success(_)));
577
578 common::await_receive_claimed(client, receive_op).await?;
579
580 while almost_equal(
584 gw_lnd_ecash_prev + expected_addition,
585 gw_lnd.client().ecash_balance(fed_id.clone()).await?,
586 5000,
587 )
588 .is_err()
589 {
590 info!("Waiting for the sending gateway's outgoing claim to settle...");
591 cmd!(client, "dev", "wait", "1").out_json().await?;
592 }
593
594 Ok(())
595}
596
597async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
598 cmd!(
599 client,
600 "--our-id",
601 peer.to_string(),
602 "--password",
603 "pass",
604 "module",
605 "lnv2",
606 "gateways",
607 "add",
608 gateway
609 )
610 .out_json()
611 .await?
612 .as_bool()
613 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
614}
615
616async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
617 cmd!(
618 client,
619 "--our-id",
620 peer.to_string(),
621 "--password",
622 "pass",
623 "module",
624 "lnv2",
625 "gateways",
626 "remove",
627 gateway
628 )
629 .out_json()
630 .await?
631 .as_bool()
632 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
633}
634
635async fn remove_all_gateways(client: &Client, peers: &[usize]) -> anyhow::Result<()> {
638 let gateways = cmd!(client, "module", "lnv2", "gateways", "list")
639 .out_json()
640 .await?
641 .as_array()
642 .expect("JSON Value is not an array")
643 .iter()
644 .map(|gateway| {
645 gateway
646 .as_str()
647 .expect("JSON Value is not a string")
648 .to_string()
649 })
650 .collect::<Vec<String>>();
651
652 for gateway in &gateways {
653 for &peer in peers {
654 assert!(remove_gateway(client, peer, gateway).await?);
655 }
656 }
657
658 Ok(())
659}
660
661const LNURL_BALANCE_WAIT_ATTEMPTS: u64 = 120;
664
665async fn wait_for_lnurl_balance(
666 client: &Client,
667 client_name: &str,
668 expected_msats: u64,
669) -> anyhow::Result<()> {
670 let mut last_balance_msats = client.balance().await?;
671
672 for attempt in 1..=LNURL_BALANCE_WAIT_ATTEMPTS {
673 if last_balance_msats < expected_msats {
674 info!(
675 client_name,
676 balance_msats = last_balance_msats,
677 expected_msats,
678 attempt,
679 "Waiting for {client_name} to receive funds via LNURL"
680 );
681 cmd!(client, "dev", "wait", "1").out_json().await?;
682 last_balance_msats = client.balance().await?;
683 } else {
684 info!(
685 client_name,
686 balance_msats = last_balance_msats,
687 expected_msats,
688 attempt,
689 "{client_name} successfully received funds via LNURL"
690 );
691 return Ok(());
692 }
693 }
694
695 if last_balance_msats < expected_msats {
696 anyhow::bail!(
697 "timed out waiting for {client_name} to receive funds via LNURL after {} attempts; last balance: {last_balance_msats} msats; expected balance: {expected_msats} msats",
698 LNURL_BALANCE_WAIT_ATTEMPTS
699 );
700 }
701
702 info!(
703 client_name,
704 balance_msats = last_balance_msats,
705 expected_msats,
706 attempt = LNURL_BALANCE_WAIT_ATTEMPTS,
707 "{client_name} successfully received funds via LNURL"
708 );
709
710 Ok(())
711}
712
713async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
714 if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
715 return Ok(());
716 }
717
718 if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
719 return Ok(());
720 }
721
722 if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
723 return Ok(());
724 }
725
726 let federation = dev_fed.fed().await?;
727
728 let gw_lnd = dev_fed.gw_lnd().await?;
729 let gw_ldk = dev_fed.gw_ldk().await?;
730
731 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
732
733 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
734
735 let client_a = federation
736 .new_joined_client("lnv2-lnurl-test-client-a")
737 .await?;
738
739 assert_module_sanity(&client_a).await?;
740
741 let client_b = federation
742 .new_joined_client("lnv2-lnurl-test-client-b")
743 .await?;
744
745 assert_module_sanity(&client_b).await?;
746
747 for (gw_send, gw_receive) in gateway_pairs {
748 info!(
749 "Testing lnurl payments: {} -> {} -> client",
750 gw_send.ln.ln_type(),
751 gw_receive.ln.ln_type()
752 );
753
754 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
755 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
756
757 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
758 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
759
760 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
761 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
762
763 let response_a = verify_payment(&verify_url_a).await?;
764 let response_b = verify_payment(&verify_url_b).await?;
765
766 assert!(!response_a.settled);
767 assert!(!response_b.settled);
768
769 assert!(response_a.preimage.is_none());
770 assert!(response_b.preimage.is_none());
771
772 gw_send.client().pay_invoice(invoice_a.clone()).await?;
773 gw_send.client().pay_invoice(invoice_b.clone()).await?;
774
775 let response_a = verify_payment(&verify_url_a).await?;
776 let response_b = verify_payment(&verify_url_b).await?;
777
778 assert!(response_a.settled);
779 assert!(response_b.settled);
780
781 verify_preimage(&response_a, &invoice_a);
782 verify_preimage(&response_b, &invoice_b);
783
784 assert_eq!(verify_task_a.await??, response_a);
785 assert_eq!(verify_task_b.await??, response_b);
786 }
787
788 wait_for_lnurl_balance(&client_a, "client A", 950 * 1000).await?;
789 wait_for_lnurl_balance(&client_b, "client B", 950 * 1000).await?;
790
791 Ok(())
792}
793
794async fn test_lnurl_recovery(dev_fed: &DevJitFed) -> anyhow::Result<()> {
802 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA {
806 return Ok(());
807 }
808
809 let federation = dev_fed.fed().await?;
810 let gw_lnd = dev_fed.gw_lnd().await?;
811 let gw_ldk = dev_fed.gw_ldk().await?;
812 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
813
814 const LNURL_AMOUNT_MSAT: u64 = 500_000;
815 const LNURL_BALANCE_TOLERANCE_MSAT: u64 = 100_000;
816
817 info!("Phase 1: Creating client and receiving via LNURL before recovery");
820
821 let client = federation
822 .new_joined_client("lnv2-lnurl-recovery-original")
823 .await?;
824
825 let lnurl = generate_lnurl(&client, &recurringd, &gw_ldk.addr).await?;
826
827 for i in 0..3 {
828 info!("Paying LNURL invoice {}/3", i + 1);
829 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
830 gw_lnd.client().pay_invoice(invoice).await?;
831 }
832
833 while almost_equal(
834 client.balance().await?,
835 3 * LNURL_AMOUNT_MSAT,
836 LNURL_BALANCE_TOLERANCE_MSAT,
837 )
838 .is_err()
839 {
840 info!("Waiting for pre-recovery LNURL payments to settle...");
841 cmd!(client, "dev", "wait", "1").out_json().await?;
842 }
843
844 let pre_recovery_balance = client.balance().await?;
845 info!("Pre-recovery balance: {pre_recovery_balance} msats");
846
847 let mnemonic = cmd!(client, "print-secret").out_json().await?["secret"]
848 .as_str()
849 .expect("secret is a string")
850 .to_owned();
851
852 info!("Phase 2: Recovering client from seed");
855
856 let restored = Client::create("lnv2-lnurl-recovery-restored").await?;
857 cmd!(
858 restored,
859 "restore",
860 "--invite-code",
861 federation.invite_code()?,
862 "--mnemonic",
863 &mnemonic
864 )
865 .run()
866 .await?;
867
868 while restored.balance().await? < pre_recovery_balance {
869 info!("Waiting for recovery to complete...");
870 cmd!(restored, "dev", "wait", "1").out_json().await?;
871 }
872
873 let post_recovery_balance = restored.balance().await?;
874 info!("Post-recovery balance: {post_recovery_balance} msats");
875
876 info!("Phase 3: Paying to the original LNURL; restored client must claim");
879
880 for i in 0..2 {
883 info!("Paying pre-recovery LNURL invoice {}/2", i + 1);
884 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
885 gw_lnd.client().pay_invoice(invoice).await?;
886 }
887
888 while almost_equal(
889 restored.balance().await?,
890 post_recovery_balance + 2 * LNURL_AMOUNT_MSAT,
891 LNURL_BALANCE_TOLERANCE_MSAT,
892 )
893 .is_err()
894 {
895 info!("Waiting for post-recovery LNURL payments to settle...");
896 cmd!(restored, "dev", "wait", "1").out_json().await?;
897 }
898
899 let final_balance = restored.balance().await?;
900 info!("Final balance: {final_balance} msats");
901
902 let operations = cmd!(restored, "list-operations", "--limit", "100")
903 .out_json()
904 .await?;
905 let lnv2_ops: Vec<_> = operations["operations"]
906 .as_array()
907 .expect("operations is an array")
908 .iter()
909 .filter(|op| op["operation_kind"].as_str() == Some("lnv2"))
910 .collect();
911 assert!(
912 lnv2_ops.len() >= 2,
913 "Expected at least 2 LNv2 operations after post-recovery receives, found {}",
914 lnv2_ops.len()
915 );
916
917 info!(
918 "LNURL recovery test passed: {} new operations, balance {final_balance} msats",
919 lnv2_ops.len()
920 );
921
922 Ok(())
923}
924
925async fn generate_lnurl(
926 client: &Client,
927 recurringd_base_url: &str,
928 gw_ldk_addr: &str,
929) -> anyhow::Result<String> {
930 cmd!(
931 client,
932 "module",
933 "lnv2",
934 "lnurl",
935 "generate",
936 recurringd_base_url,
937 "--gateway",
938 gw_ldk_addr,
939 )
940 .out_json()
941 .await
942 .map(|s| s.as_str().unwrap().to_owned())
943}
944
945fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
946 let preimage = response.preimage.expect("Payment should be settled");
947
948 let payment_hash = preimage.consensus_hash::<sha256::Hash>();
949
950 assert_eq!(payment_hash, *invoice.payment_hash());
951}
952
953async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
954 reqwest::get(verify_url)
955 .await?
956 .json::<LnurlResponse<VerifyResponse>>()
957 .await?
958 .into_result()
959 .map_err(anyhow::Error::msg)
960}
961
962async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
963 reqwest::get(format!("{verify_url}?wait"))
964 .await?
965 .json::<LnurlResponse<VerifyResponse>>()
966 .await?
967 .into_result()
968 .map_err(anyhow::Error::msg)
969}
970
971#[derive(Deserialize, Clone)]
972struct LnUrlPayResponse {
973 callback: String,
974}
975
976#[derive(Deserialize, Clone)]
977struct LnUrlPayInvoiceResponse {
978 pr: Bolt11Invoice,
979 verify: String,
980}
981
982async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
983 let url = parse_lnurl(&lnurl).ok_or_else(|| anyhow::anyhow!("Invalid LNURL"))?;
984
985 let response = reqwest::get(url).await?.json::<LnUrlPayResponse>().await?;
986
987 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
988
989 let response = reqwest::get(callback_url)
990 .await?
991 .json::<LnUrlPayInvoiceResponse>()
992 .await?;
993
994 ensure!(
995 response.pr.amount_milli_satoshis() == Some(amount_msat),
996 "Invoice amount is not set"
997 );
998
999 Ok((response.pr, response.verify))
1000}
1001
1002async fn test_iroh_payment(
1003 client: &Client,
1004 gw_lnd: &Gatewayd,
1005 gw_ldk: &Gatewayd,
1006 online_peers: &[usize],
1007) -> anyhow::Result<()> {
1008 info!("Testing iroh payment...");
1009 remove_all_gateways(client, online_peers).await?;
1012 for &peer in online_peers {
1013 add_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1014 }
1015
1016 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1019 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1020 {
1021 for &peer in online_peers {
1022 add_gateway(client, peer, &gw_lnd.addr).await?;
1023 }
1024 }
1025
1026 let invoice = gw_ldk.client().create_invoice(5_000_000).await?;
1027
1028 let send_op = serde_json::from_value::<OperationId>(
1029 cmd!(client, "module", "lnv2", "send", invoice,)
1030 .out_json()
1031 .await?,
1032 )?;
1033
1034 let send_state = common::await_send(client, send_op).await?;
1035 assert!(
1036 matches!(send_state, FinalSendOperationState::Success(_)),
1037 "unexpected send state: {send_state:?}"
1038 );
1039
1040 let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1041 cmd!(client, "module", "lnv2", "receive", "5000000",)
1042 .out_json()
1043 .await?,
1044 )?;
1045
1046 gw_ldk.client().pay_invoice(invoice).await?;
1047 common::await_receive_claimed(client, receive_op).await?;
1048
1049 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1050 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1051 {
1052 for &peer in online_peers {
1053 remove_gateway(client, peer, &gw_lnd.addr).await?;
1054 }
1055 }
1056
1057 for &peer in online_peers {
1058 remove_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1059 }
1060
1061 Ok(())
1062}