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