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 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    /// Run gateway registration tests
53    GatewayRegistration,
54    /// Run payment tests
55    Payments,
56    /// Run LNURL pay tests
57    LnurlPay,
58    /// Test LNURL receives after recovery from seed
59    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                    // Run all tests if no subcommand is specified
90                    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    // Gateway pays: 1_000 msat LNv2 federation base fee. Gateway receives:
359    // 1_000_000 payment.
360    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    let gw_lnd_ecash_after = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
412
413    almost_equal(
414        gw_lnd_ecash_prev + expected_addition,
415        gw_lnd_ecash_after,
416        5000,
417    )
418    .unwrap();
419
420    Ok(())
421}
422
423async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
424    cmd!(
425        client,
426        "--our-id",
427        peer.to_string(),
428        "--password",
429        "pass",
430        "module",
431        "lnv2",
432        "gateways",
433        "add",
434        gateway
435    )
436    .out_json()
437    .await?
438    .as_bool()
439    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
440}
441
442async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
443    cmd!(
444        client,
445        "--our-id",
446        peer.to_string(),
447        "--password",
448        "pass",
449        "module",
450        "lnv2",
451        "gateways",
452        "remove",
453        gateway
454    )
455    .out_json()
456    .await?
457    .as_bool()
458    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
459}
460
461async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
462    if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
463        return Ok(());
464    }
465
466    if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
467        return Ok(());
468    }
469
470    if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
471        return Ok(());
472    }
473
474    let federation = dev_fed.fed().await?;
475
476    let gw_lnd = dev_fed.gw_lnd().await?;
477    let gw_ldk = dev_fed.gw_ldk().await?;
478
479    let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
480
481    let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
482
483    let client_a = federation
484        .new_joined_client("lnv2-lnurl-test-client-a")
485        .await?;
486
487    assert_module_sanity(&client_a).await?;
488
489    let client_b = federation
490        .new_joined_client("lnv2-lnurl-test-client-b")
491        .await?;
492
493    assert_module_sanity(&client_b).await?;
494
495    for (gw_send, gw_receive) in gateway_pairs {
496        info!(
497            "Testing lnurl payments: {} -> {} -> client",
498            gw_send.ln.ln_type(),
499            gw_receive.ln.ln_type()
500        );
501
502        let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
503        let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
504
505        let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
506        let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
507
508        let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
509        let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
510
511        let response_a = verify_payment(&verify_url_a).await?;
512        let response_b = verify_payment(&verify_url_b).await?;
513
514        assert!(!response_a.settled);
515        assert!(!response_b.settled);
516
517        assert!(response_a.preimage.is_none());
518        assert!(response_b.preimage.is_none());
519
520        gw_send.client().pay_invoice(invoice_a.clone()).await?;
521        gw_send.client().pay_invoice(invoice_b.clone()).await?;
522
523        let response_a = verify_payment(&verify_url_a).await?;
524        let response_b = verify_payment(&verify_url_b).await?;
525
526        assert!(response_a.settled);
527        assert!(response_b.settled);
528
529        verify_preimage(&response_a, &invoice_a);
530        verify_preimage(&response_b, &invoice_b);
531
532        assert_eq!(verify_task_a.await??, response_a);
533        assert_eq!(verify_task_b.await??, response_b);
534    }
535
536    while client_a.balance().await? < 950 * 1000 {
537        info!("Waiting for client A to receive funds via LNURL...");
538
539        cmd!(client_a, "dev", "wait", "1").out_json().await?;
540    }
541
542    info!("Client A successfully received funds via LNURL!");
543
544    while client_b.balance().await? < 950 * 1000 {
545        info!("Waiting for client B to receive funds via LNURL...");
546
547        cmd!(client_b, "dev", "wait", "1").out_json().await?;
548    }
549
550    info!("Client B successfully received funds via LNURL!");
551
552    Ok(())
553}
554
555/// Tests LNURL receives after recovery from seed.
556///
557/// LNv2 uses `NoModuleBackup`, so recovery restores funds via the mint module
558/// but not the operation log or LNv2 state. Verifies:
559/// 1. Balance is fully recovered.
560/// 2. Payments to a pre-recovery LNURL are still claimed by the restored
561///    client.
562async fn test_lnurl_recovery(dev_fed: &DevJitFed) -> anyhow::Result<()> {
563    // Before v0.10.0 the CLI registered LNURLs via a POST to the recurringd
564    // server.  That endpoint no longer exists, so old CLIs cannot generate
565    // LNURLs against the current recurringdv2.
566    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA {
567        return Ok(());
568    }
569
570    let federation = dev_fed.fed().await?;
571    let gw_lnd = dev_fed.gw_lnd().await?;
572    let gw_ldk = dev_fed.gw_ldk().await?;
573    let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
574
575    const LNURL_AMOUNT_MSAT: u64 = 500_000;
576    const LNURL_BALANCE_TOLERANCE_MSAT: u64 = 100_000;
577
578    // ── Phase 1: Pre-recovery LNURL receives ──────────────────────────
579
580    info!("Phase 1: Creating client and receiving via LNURL before recovery");
581
582    let client = federation
583        .new_joined_client("lnv2-lnurl-recovery-original")
584        .await?;
585
586    let lnurl = generate_lnurl(&client, &recurringd, &gw_ldk.addr).await?;
587
588    for i in 0..3 {
589        info!("Paying LNURL invoice {}/3", i + 1);
590        let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
591        gw_lnd.client().pay_invoice(invoice).await?;
592    }
593
594    while almost_equal(
595        client.balance().await?,
596        3 * LNURL_AMOUNT_MSAT,
597        LNURL_BALANCE_TOLERANCE_MSAT,
598    )
599    .is_err()
600    {
601        info!("Waiting for pre-recovery LNURL payments to settle...");
602        cmd!(client, "dev", "wait", "1").out_json().await?;
603    }
604
605    let pre_recovery_balance = client.balance().await?;
606    info!("Pre-recovery balance: {pre_recovery_balance} msats");
607
608    let mnemonic = cmd!(client, "print-secret").out_json().await?["secret"]
609        .as_str()
610        .expect("secret is a string")
611        .to_owned();
612
613    // ── Phase 2: Recovery ─────────────────────────────────────────────
614
615    info!("Phase 2: Recovering client from seed");
616
617    let restored = Client::create("lnv2-lnurl-recovery-restored").await?;
618    cmd!(
619        restored,
620        "restore",
621        "--invite-code",
622        federation.invite_code()?,
623        "--mnemonic",
624        &mnemonic
625    )
626    .run()
627    .await?;
628
629    while restored.balance().await? < pre_recovery_balance {
630        info!("Waiting for recovery to complete...");
631        cmd!(restored, "dev", "wait", "1").out_json().await?;
632    }
633
634    let post_recovery_balance = restored.balance().await?;
635    info!("Post-recovery balance: {post_recovery_balance} msats");
636
637    // ── Phase 3: Post-recovery LNURL receives ─────────────────────────
638
639    info!("Phase 3: Paying to the original LNURL; restored client must claim");
640
641    // Reuse the Phase 1 `lnurl` on purpose: this is what a third party would
642    // have saved pre-recovery, and it must still pay the restored client.
643    for i in 0..2 {
644        info!("Paying pre-recovery LNURL invoice {}/2", i + 1);
645        let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
646        gw_lnd.client().pay_invoice(invoice).await?;
647    }
648
649    while almost_equal(
650        restored.balance().await?,
651        post_recovery_balance + 2 * LNURL_AMOUNT_MSAT,
652        LNURL_BALANCE_TOLERANCE_MSAT,
653    )
654    .is_err()
655    {
656        info!("Waiting for post-recovery LNURL payments to settle...");
657        cmd!(restored, "dev", "wait", "1").out_json().await?;
658    }
659
660    let final_balance = restored.balance().await?;
661    info!("Final balance: {final_balance} msats");
662
663    let operations = cmd!(restored, "list-operations", "--limit", "100")
664        .out_json()
665        .await?;
666    let lnv2_ops: Vec<_> = operations["operations"]
667        .as_array()
668        .expect("operations is an array")
669        .iter()
670        .filter(|op| op["operation_kind"].as_str() == Some("lnv2"))
671        .collect();
672    assert!(
673        lnv2_ops.len() >= 2,
674        "Expected at least 2 LNv2 operations after post-recovery receives, found {}",
675        lnv2_ops.len()
676    );
677
678    info!(
679        "LNURL recovery test passed: {} new operations, balance {final_balance} msats",
680        lnv2_ops.len()
681    );
682
683    Ok(())
684}
685
686async fn generate_lnurl(
687    client: &Client,
688    recurringd_base_url: &str,
689    gw_ldk_addr: &str,
690) -> anyhow::Result<String> {
691    cmd!(
692        client,
693        "module",
694        "lnv2",
695        "lnurl",
696        "generate",
697        recurringd_base_url,
698        "--gateway",
699        gw_ldk_addr,
700    )
701    .out_json()
702    .await
703    .map(|s| s.as_str().unwrap().to_owned())
704}
705
706fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
707    let preimage = response.preimage.expect("Payment should be settled");
708
709    let payment_hash = preimage.consensus_hash::<sha256::Hash>();
710
711    assert_eq!(payment_hash, *invoice.payment_hash());
712}
713
714async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
715    reqwest::get(verify_url)
716        .await?
717        .json::<LnurlResponse<VerifyResponse>>()
718        .await?
719        .into_result()
720        .map_err(anyhow::Error::msg)
721}
722
723async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
724    reqwest::get(format!("{verify_url}?wait"))
725        .await?
726        .json::<LnurlResponse<VerifyResponse>>()
727        .await?
728        .into_result()
729        .map_err(anyhow::Error::msg)
730}
731
732#[derive(Deserialize, Clone)]
733struct LnUrlPayResponse {
734    callback: String,
735}
736
737#[derive(Deserialize, Clone)]
738struct LnUrlPayInvoiceResponse {
739    pr: Bolt11Invoice,
740    verify: String,
741}
742
743async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
744    let url = parse_lnurl(&lnurl).ok_or_else(|| anyhow::anyhow!("Invalid LNURL"))?;
745
746    let response = reqwest::get(url).await?.json::<LnUrlPayResponse>().await?;
747
748    let callback_url = format!("{}?amount={}", response.callback, amount_msat);
749
750    let response = reqwest::get(callback_url)
751        .await?
752        .json::<LnUrlPayInvoiceResponse>()
753        .await?;
754
755    ensure!(
756        response.pr.amount_milli_satoshis() == Some(amount_msat),
757        "Invoice amount is not set"
758    );
759
760    Ok((response.pr, response.verify))
761}
762
763async fn test_iroh_payment(
764    client: &Client,
765    gw_lnd: &Gatewayd,
766    gw_ldk: &Gatewayd,
767    online_peers: &[usize],
768) -> anyhow::Result<()> {
769    info!("Testing iroh payment...");
770    for &peer in online_peers {
771        add_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
772    }
773
774    // If the client is below v0.10.0, also add the HTTP address so that the client
775    // can fallback to using that, since the iroh gateway will fail.
776    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
777        || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
778    {
779        for &peer in online_peers {
780            add_gateway(client, peer, &gw_lnd.addr).await?;
781        }
782    }
783
784    let invoice = gw_ldk.client().create_invoice(5_000_000).await?;
785
786    let send_op = serde_json::from_value::<OperationId>(
787        cmd!(client, "module", "lnv2", "send", invoice,)
788            .out_json()
789            .await?,
790    )?;
791
792    let send_state = common::await_send(client, send_op).await?;
793    assert!(
794        matches!(send_state, FinalSendOperationState::Success(_)),
795        "unexpected send state: {send_state:?}"
796    );
797
798    let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
799        cmd!(client, "module", "lnv2", "receive", "5000000",)
800            .out_json()
801            .await?,
802    )?;
803
804    gw_ldk.client().pay_invoice(invoice).await?;
805    common::await_receive_claimed(client, receive_op).await?;
806
807    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
808        || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
809    {
810        for &peer in online_peers {
811            remove_gateway(client, peer, &gw_lnd.addr).await?;
812        }
813    }
814
815    for &peer in online_peers {
816        remove_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
817    }
818
819    Ok(())
820}