lnv2_module_tests/
tests.rs

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