Skip to main content

lnv2_module_tests/
tests.rs

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