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    // The sending gateway claims its outgoing contract in the background after
412    // returning the preimage to the sender, so its ecash balance is credited
413    // asynchronously. Wait for the claim to settle before asserting the fee.
414    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
466// Keep this below the 310s `fm-run-test` timeout so LNURL flakes fail with
467// useful balance diagnostics instead of a generic test timeout.
468const 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
599/// Tests LNURL receives after recovery from seed.
600///
601/// LNv2 uses `NoModuleBackup`, so recovery restores funds via the mint module
602/// but not the operation log or LNv2 state. Verifies:
603/// 1. Balance is fully recovered.
604/// 2. Payments to a pre-recovery LNURL are still claimed by the restored
605///    client.
606async fn test_lnurl_recovery(dev_fed: &DevJitFed) -> anyhow::Result<()> {
607    // Before v0.10.0 the CLI registered LNURLs via a POST to the recurringd
608    // server.  That endpoint no longer exists, so old CLIs cannot generate
609    // LNURLs against the current recurringdv2.
610    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    // ── Phase 1: Pre-recovery LNURL receives ──────────────────────────
623
624    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    // ── Phase 2: Recovery ─────────────────────────────────────────────
658
659    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    // ── Phase 3: Post-recovery LNURL receives ─────────────────────────
682
683    info!("Phase 3: Paying to the original LNURL; restored client must claim");
684
685    // Reuse the Phase 1 `lnurl` on purpose: this is what a third party would
686    // have saved pre-recovery, and it must still pay the restored client.
687    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 the client is below v0.10.0, also add the HTTP address so that the client
819    // can fallback to using that, since the iroh gateway will fail.
820    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}