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