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::{
8    VERSION_0_9_0_ALPHA, VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA,
9};
10use devimint::{Gatewayd, cmd, util};
11use fedimint_core::core::OperationId;
12use fedimint_core::encoding::Encodable;
13use fedimint_core::task::{self};
14use fedimint_core::util::{backoff_util, retry};
15use fedimint_lnurl::{LnurlResponse, VerifyResponse, parse_lnurl};
16use fedimint_lnv2_client::FinalSendOperationState;
17use lightning_invoice::Bolt11Invoice;
18use serde::Deserialize;
19use substring::Substring;
20use tokio::try_join;
21use tracing::info;
22
23#[path = "common.rs"]
24mod common;
25
26async fn module_is_present(client: &Client, kind: &str) -> anyhow::Result<bool> {
27    let modules = cmd!(client, "module").out_json().await?;
28
29    let modules = modules["list"].as_array().expect("module list is an array");
30
31    Ok(modules.iter().any(|m| m["kind"].as_str() == Some(kind)))
32}
33
34async fn assert_module_sanity(client: &Client) -> anyhow::Result<()> {
35    if !devimint::util::is_backwards_compatibility_test() {
36        ensure!(
37            !module_is_present(client, "ln").await?,
38            "ln module should not be present"
39        );
40    }
41
42    Ok(())
43}
44
45#[derive(Parser)]
46#[command(name = "lnv2-module-tests")]
47#[command(about = "LNv2 module integration tests", long_about = None)]
48struct Cli {
49    #[command(subcommand)]
50    command: Option<Commands>,
51}
52
53#[derive(Subcommand)]
54enum Commands {
55    /// Run gateway registration tests
56    GatewayRegistration,
57    /// Run payment tests
58    Payments,
59    /// Run LNURL pay tests
60    LnurlPay,
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                None => {
86                    // Run all tests if no subcommand is specified
87                    test_gateway_registration(&dev_fed).await?;
88                    test_payments(&dev_fed).await?;
89                    test_lnurl_pay(&dev_fed).await?;
90                }
91            }
92
93            info!("Testing LNV2 is complete!");
94
95            Ok(())
96        })
97        .await
98}
99
100async fn pegin_gateways(dev_fed: &DevJitFed) -> anyhow::Result<()> {
101    info!("Pegging-in gateways...");
102
103    let federation = dev_fed.fed().await?;
104
105    let gw_lnd = dev_fed.gw_lnd().await?;
106    let gw_ldk = dev_fed.gw_ldk().await?;
107
108    federation
109        .pegin_gateways(1_000_000, vec![gw_lnd, gw_ldk])
110        .await?;
111
112    Ok(())
113}
114
115async fn test_gateway_registration(dev_fed: &DevJitFed) -> anyhow::Result<()> {
116    let client = dev_fed
117        .fed()
118        .await?
119        .new_joined_client("lnv2-test-gateway-registration-client")
120        .await?;
121
122    assert_module_sanity(&client).await?;
123
124    let gw_lnd = dev_fed.gw_lnd().await?;
125    let gw_ldk = dev_fed.gw_ldk_connected().await?;
126
127    let gateways = [gw_lnd.addr.clone(), gw_ldk.addr.clone()];
128
129    info!("Testing registration of gateways...");
130
131    for gateway in &gateways {
132        for peer in 0..dev_fed.fed().await?.members.len() {
133            assert!(add_gateway(&client, peer, gateway).await?);
134        }
135    }
136
137    assert_eq!(
138        cmd!(client, "module", "lnv2", "gateways", "list")
139            .out_json()
140            .await?
141            .as_array()
142            .expect("JSON Value is not an array")
143            .len(),
144        2
145    );
146
147    assert_eq!(
148        cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
149            .out_json()
150            .await?
151            .as_array()
152            .expect("JSON Value is not an array")
153            .len(),
154        2
155    );
156
157    info!("Testing selection of gateways...");
158
159    assert!(
160        gateways.contains(
161            &cmd!(client, "module", "lnv2", "gateways", "select")
162                .out_json()
163                .await?
164                .as_str()
165                .expect("JSON Value is not a string")
166                .to_string()
167        )
168    );
169
170    cmd!(client, "module", "lnv2", "gateways", "map")
171        .out_json()
172        .await?;
173
174    for _ in 0..10 {
175        for gateway in &gateways {
176            let invoice = common::receive(&client, gateway, 1_000_000).await?.0;
177
178            assert_eq!(
179                cmd!(
180                    client,
181                    "module",
182                    "lnv2",
183                    "gateways",
184                    "select",
185                    "--invoice",
186                    invoice.to_string()
187                )
188                .out_json()
189                .await?
190                .as_str()
191                .expect("JSON Value is not a string"),
192                gateway
193            )
194        }
195    }
196
197    info!("Testing deregistration of gateways...");
198
199    for gateway in &gateways {
200        for peer in 0..dev_fed.fed().await?.members.len() {
201            assert!(remove_gateway(&client, peer, gateway).await?);
202        }
203    }
204
205    assert!(
206        cmd!(client, "module", "lnv2", "gateways", "list")
207            .out_json()
208            .await?
209            .as_array()
210            .expect("JSON Value is not an array")
211            .is_empty(),
212    );
213
214    assert!(
215        cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
216            .out_json()
217            .await?
218            .as_array()
219            .expect("JSON Value is not an array")
220            .is_empty()
221    );
222
223    Ok(())
224}
225
226async fn test_payments(dev_fed: &DevJitFed) -> anyhow::Result<()> {
227    let federation = dev_fed.fed().await?;
228
229    let client = federation
230        .new_joined_client("lnv2-test-payments-client")
231        .await?;
232
233    assert_module_sanity(&client).await?;
234
235    federation.pegin_client(10_000, &client).await?;
236
237    almost_equal(client.balance().await?, 10_000 * 1000, 500_000).unwrap();
238
239    let gw_lnd = dev_fed.gw_lnd().await?;
240    let gw_ldk = dev_fed.gw_ldk().await?;
241    let lnd = dev_fed.lnd().await?;
242
243    let (hold_preimage, hold_invoice, hold_payment_hash) = lnd.create_hold_invoice(60000).await?;
244
245    let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
246
247    let gateway_matrix = [
248        (gw_lnd, gw_lnd),
249        (gw_lnd, gw_ldk),
250        (gw_ldk, gw_lnd),
251        (gw_ldk, gw_ldk),
252    ];
253
254    info!("Testing refund of circular payments...");
255
256    for (gw_send, gw_receive) in gateway_matrix {
257        info!(
258            "Testing refund of payment: client -> {} -> {} -> client",
259            gw_send.ln.ln_type(),
260            gw_receive.ln.ln_type()
261        );
262
263        let invoice = common::receive(&client, &gw_receive.addr, 1_000_000)
264            .await?
265            .0;
266
267        common::send(
268            &client,
269            &gw_send.addr,
270            &invoice.to_string(),
271            FinalSendOperationState::Refunded,
272        )
273        .await?;
274    }
275
276    pegin_gateways(dev_fed).await?;
277
278    info!("Testing circular payments...");
279
280    for (gw_send, gw_receive) in gateway_matrix {
281        info!(
282            "Testing payment: client -> {} -> {} -> client",
283            gw_send.ln.ln_type(),
284            gw_receive.ln.ln_type()
285        );
286
287        let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
288
289        common::send(
290            &client,
291            &gw_send.addr,
292            &invoice.to_string(),
293            FinalSendOperationState::Success,
294        )
295        .await?;
296
297        common::await_receive_claimed(&client, receive_op).await?;
298    }
299
300    info!("Testing payments from client to gateways...");
301
302    for (gw_send, gw_receive) in gateway_pairs {
303        info!(
304            "Testing payment: client -> {} -> {}",
305            gw_send.ln.ln_type(),
306            gw_receive.ln.ln_type()
307        );
308
309        let invoice = gw_receive.client().create_invoice(1_000_000).await?;
310
311        common::send(
312            &client,
313            &gw_send.addr,
314            &invoice.to_string(),
315            FinalSendOperationState::Success,
316        )
317        .await?;
318    }
319
320    info!("Testing payments from gateways to client...");
321
322    for (gw_send, gw_receive) in gateway_pairs {
323        info!(
324            "Testing payment: {} -> {} -> client",
325            gw_send.ln.ln_type(),
326            gw_receive.ln.ln_type()
327        );
328
329        let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
330
331        gw_send.client().pay_invoice(invoice).await?;
332
333        common::await_receive_claimed(&client, receive_op).await?;
334    }
335
336    retry(
337        "Waiting for the full balance to become available to the client".to_string(),
338        backoff_util::background_backoff(),
339        || async {
340            ensure!(client.balance().await? >= 9000 * 1000);
341
342            Ok(())
343        },
344    )
345    .await?;
346
347    info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
348
349    try_join!(
350        common::send(
351            &client,
352            &gw_ldk.addr,
353            &hold_invoice,
354            FinalSendOperationState::Success
355        ),
356        lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
357    )?;
358
359    info!("Testing LNv2 lightning fees...");
360
361    let fed_id = federation.calculate_federation_id();
362
363    gw_lnd
364        .client()
365        .set_federation_routing_fee(fed_id.clone(), 0, 0)
366        .await?;
367
368    gw_lnd
369        .client()
370        .set_federation_transaction_fee(fed_id.clone(), 0, 0)
371        .await?;
372
373    if util::FedimintdCmd::version_or_default().await >= *VERSION_0_9_0_ALPHA {
374        // Gateway pays: 1_000 msat LNv2 federation base fee. Gateway receives:
375        // 1_000_000 payment.
376        test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
377    } else {
378        // Gateway pays: 1_000 msat LNv2 federation base fee, 100 msat LNv2 federation
379        // relative fee. Gateway receives: 1_000_000 payment.
380        test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000 - 100).await?;
381    }
382
383    let online_peers: Vec<usize> = federation.members.keys().copied().collect();
384
385    test_iroh_payment(&client, gw_lnd, gw_ldk, &online_peers).await?;
386
387    info!("Testing payment summary...");
388
389    let lnd_payment_summary = gw_lnd.client().payment_summary().await?;
390
391    assert_eq!(lnd_payment_summary.outgoing.total_success, 5);
392    assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
393    assert_eq!(lnd_payment_summary.incoming.total_success, 4);
394    assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
395
396    assert!(lnd_payment_summary.outgoing.median_latency.is_some());
397    assert!(lnd_payment_summary.outgoing.average_latency.is_some());
398    assert!(lnd_payment_summary.incoming.median_latency.is_some());
399    assert!(lnd_payment_summary.incoming.average_latency.is_some());
400
401    let ldk_payment_summary = gw_ldk.client().payment_summary().await?;
402
403    assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
404    assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
405    assert_eq!(ldk_payment_summary.incoming.total_success, 4);
406    assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
407
408    assert!(ldk_payment_summary.outgoing.median_latency.is_some());
409    assert!(ldk_payment_summary.outgoing.average_latency.is_some());
410    assert!(ldk_payment_summary.incoming.median_latency.is_some());
411    assert!(ldk_payment_summary.incoming.average_latency.is_some());
412
413    Ok(())
414}
415
416async fn test_fees(
417    fed_id: String,
418    client: &Client,
419    gw_lnd: &Gatewayd,
420    gw_ldk: &Gatewayd,
421    expected_addition: u64,
422) -> anyhow::Result<()> {
423    let gw_lnd_ecash_prev = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
424
425    let (invoice, receive_op) = common::receive(client, &gw_ldk.addr, 1_000_000).await?;
426
427    common::send(
428        client,
429        &gw_lnd.addr,
430        &invoice.to_string(),
431        FinalSendOperationState::Success,
432    )
433    .await?;
434
435    common::await_receive_claimed(client, receive_op).await?;
436
437    let gw_lnd_ecash_after = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
438
439    almost_equal(
440        gw_lnd_ecash_prev + expected_addition,
441        gw_lnd_ecash_after,
442        5000,
443    )
444    .unwrap();
445
446    Ok(())
447}
448
449async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
450    cmd!(
451        client,
452        "--our-id",
453        peer.to_string(),
454        "--password",
455        "pass",
456        "module",
457        "lnv2",
458        "gateways",
459        "add",
460        gateway
461    )
462    .out_json()
463    .await?
464    .as_bool()
465    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
466}
467
468async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
469    cmd!(
470        client,
471        "--our-id",
472        peer.to_string(),
473        "--password",
474        "pass",
475        "module",
476        "lnv2",
477        "gateways",
478        "remove",
479        gateway
480    )
481    .out_json()
482    .await?
483    .as_bool()
484    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
485}
486
487async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
488    if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
489        return Ok(());
490    }
491
492    if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
493        return Ok(());
494    }
495
496    if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
497        return Ok(());
498    }
499
500    let federation = dev_fed.fed().await?;
501
502    let gw_lnd = dev_fed.gw_lnd().await?;
503    let gw_ldk = dev_fed.gw_ldk().await?;
504
505    let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
506
507    let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
508
509    let client_a = federation
510        .new_joined_client("lnv2-lnurl-test-client-a")
511        .await?;
512
513    assert_module_sanity(&client_a).await?;
514
515    let client_b = federation
516        .new_joined_client("lnv2-lnurl-test-client-b")
517        .await?;
518
519    assert_module_sanity(&client_b).await?;
520
521    for (gw_send, gw_receive) in gateway_pairs {
522        info!(
523            "Testing lnurl payments: {} -> {} -> client",
524            gw_send.ln.ln_type(),
525            gw_receive.ln.ln_type()
526        );
527
528        let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
529        let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
530
531        let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
532        let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
533
534        let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
535        let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
536
537        let response_a = verify_payment(&verify_url_a).await?;
538        let response_b = verify_payment(&verify_url_b).await?;
539
540        assert!(!response_a.settled);
541        assert!(!response_b.settled);
542
543        assert!(response_a.preimage.is_none());
544        assert!(response_b.preimage.is_none());
545
546        gw_send.client().pay_invoice(invoice_a.clone()).await?;
547        gw_send.client().pay_invoice(invoice_b.clone()).await?;
548
549        let response_a = verify_payment(&verify_url_a).await?;
550        let response_b = verify_payment(&verify_url_b).await?;
551
552        assert!(response_a.settled);
553        assert!(response_b.settled);
554
555        verify_preimage(&response_a, &invoice_a);
556        verify_preimage(&response_b, &invoice_b);
557
558        assert_eq!(verify_task_a.await??, response_a);
559        assert_eq!(verify_task_b.await??, response_b);
560    }
561
562    while client_a.balance().await? < 950 * 1000 {
563        info!("Waiting for client A to receive funds via LNURL...");
564
565        cmd!(client_a, "dev", "wait", "1").out_json().await?;
566    }
567
568    info!("Client A successfully received funds via LNURL!");
569
570    while client_b.balance().await? < 950 * 1000 {
571        info!("Waiting for client B to receive funds via LNURL...");
572
573        cmd!(client_b, "dev", "wait", "1").out_json().await?;
574    }
575
576    info!("Client B successfully received funds via LNURL!");
577
578    Ok(())
579}
580
581async fn generate_lnurl(
582    client: &Client,
583    recurringd_base_url: &str,
584    gw_ldk_addr: &str,
585) -> anyhow::Result<String> {
586    cmd!(
587        client,
588        "module",
589        "lnv2",
590        "lnurl",
591        "generate",
592        recurringd_base_url,
593        "--gateway",
594        gw_ldk_addr,
595    )
596    .out_json()
597    .await
598    .map(|s| s.as_str().unwrap().to_owned())
599}
600
601fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
602    let preimage = response.preimage.expect("Payment should be settled");
603
604    let payment_hash = preimage.consensus_hash::<sha256::Hash>();
605
606    assert_eq!(payment_hash, *invoice.payment_hash());
607}
608
609async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
610    reqwest::get(verify_url)
611        .await?
612        .json::<LnurlResponse<VerifyResponse>>()
613        .await?
614        .into_result()
615        .map_err(anyhow::Error::msg)
616}
617
618async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
619    reqwest::get(format!("{verify_url}?wait"))
620        .await?
621        .json::<LnurlResponse<VerifyResponse>>()
622        .await?
623        .into_result()
624        .map_err(anyhow::Error::msg)
625}
626
627#[derive(Deserialize, Clone)]
628struct LnUrlPayResponse {
629    callback: String,
630}
631
632#[derive(Deserialize, Clone)]
633struct LnUrlPayInvoiceResponse {
634    pr: Bolt11Invoice,
635    verify: String,
636}
637
638async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
639    let url = parse_lnurl(&lnurl).ok_or_else(|| anyhow::anyhow!("Invalid LNURL"))?;
640
641    let response = reqwest::get(url).await?.json::<LnUrlPayResponse>().await?;
642
643    let callback_url = format!("{}?amount={}", response.callback, amount_msat);
644
645    let response = reqwest::get(callback_url)
646        .await?
647        .json::<LnUrlPayInvoiceResponse>()
648        .await?;
649
650    ensure!(
651        response.pr.amount_milli_satoshis() == Some(amount_msat),
652        "Invoice amount is not set"
653    );
654
655    Ok((response.pr, response.verify))
656}
657
658async fn test_iroh_payment(
659    client: &Client,
660    gw_lnd: &Gatewayd,
661    gw_ldk: &Gatewayd,
662    online_peers: &[usize],
663) -> anyhow::Result<()> {
664    info!("Testing iroh payment...");
665    for &peer in online_peers {
666        add_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
667    }
668
669    // If the client is below v0.10.0, also add the HTTP address so that the client
670    // can fallback to using that, since the iroh gateway will fail.
671    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
672        || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
673    {
674        for &peer in online_peers {
675            add_gateway(client, peer, &gw_lnd.addr).await?;
676        }
677    }
678
679    let invoice = gw_ldk.client().create_invoice(5_000_000).await?;
680
681    let send_op = serde_json::from_value::<OperationId>(
682        cmd!(client, "module", "lnv2", "send", invoice,)
683            .out_json()
684            .await?,
685    )?;
686
687    assert_eq!(
688        cmd!(
689            client,
690            "module",
691            "lnv2",
692            "await-send",
693            serde_json::to_string(&send_op)?.substring(1, 65)
694        )
695        .out_json()
696        .await?,
697        serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
698    );
699
700    let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
701        cmd!(client, "module", "lnv2", "receive", "5000000",)
702            .out_json()
703            .await?,
704    )?;
705
706    gw_ldk.client().pay_invoice(invoice).await?;
707    common::await_receive_claimed(client, receive_op).await?;
708
709    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
710        || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
711    {
712        for &peer in online_peers {
713            remove_gateway(client, peer, &gw_lnd.addr).await?;
714        }
715    }
716
717    for &peer in online_peers {
718        remove_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
719    }
720
721    Ok(())
722}