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::{
10    VERSION_0_9_0_ALPHA, VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA,
11};
12use devimint::{Gatewayd, cmd, util};
13use fedimint_core::core::OperationId;
14use fedimint_core::encoding::Encodable;
15use fedimint_core::task::{self};
16use fedimint_core::util::{backoff_util, retry};
17use fedimint_lnv2_client::FinalSendOperationState;
18use fedimint_lnv2_common::lnurl::VerifyResponse;
19use lightning_invoice::Bolt11Invoice;
20use lnurl::lnurl::LnUrl;
21use serde::Deserialize;
22use substring::Substring;
23use tokio::try_join;
24use tracing::info;
25
26#[path = "common.rs"]
27mod common;
28
29#[derive(Parser)]
30#[command(name = "lnv2-module-tests")]
31#[command(about = "LNv2 module integration tests", long_about = None)]
32struct Cli {
33    #[command(subcommand)]
34    command: Option<Commands>,
35}
36
37#[derive(Subcommand)]
38enum Commands {
39    /// Run gateway registration tests
40    GatewayRegistration,
41    /// Run payment tests
42    Payments,
43    /// Run LNURL pay tests
44    LnurlPay,
45}
46
47#[tokio::main]
48async fn main() -> anyhow::Result<()> {
49    let cli = Cli::parse();
50
51    devimint::run_devfed_test()
52        .call(|dev_fed, _process_mgr| async move {
53            if !devimint::util::supports_lnv2() {
54                info!("lnv2 is disabled, skipping");
55                return Ok(());
56            }
57
58            if !devimint::util::is_backwards_compatibility_test() {
59                info!("Verifying that LNv1 module is disabled...");
60
61                ensure!(
62                    !devimint::util::supports_lnv1(),
63                    "LNv1 module should be disabled when not in backwards compatibility test"
64                );
65            }
66
67            match &cli.command {
68                Some(Commands::GatewayRegistration) => {
69                    test_gateway_registration(&dev_fed).await?;
70                }
71                Some(Commands::Payments) => {
72                    test_payments(&dev_fed).await?;
73                }
74                Some(Commands::LnurlPay) => {
75                    pegin_gateways(&dev_fed).await?;
76                    test_lnurl_pay(&dev_fed).await?;
77                }
78                None => {
79                    // Run all tests if no subcommand is specified
80                    test_gateway_registration(&dev_fed).await?;
81                    test_payments(&dev_fed).await?;
82                    test_lnurl_pay(&dev_fed).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) -> anyhow::Result<()> {
473    if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
474        return Ok(());
475    }
476
477    if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
478        return Ok(());
479    }
480
481    if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
482        return Ok(());
483    }
484
485    let federation = dev_fed.fed().await?;
486
487    let gw_lnd = dev_fed.gw_lnd().await?;
488    let gw_ldk = dev_fed.gw_ldk().await?;
489
490    let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
491
492    let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
493
494    let client_a = federation
495        .new_joined_client("lnv2-lnurl-test-client-a")
496        .await?;
497
498    let client_b = federation
499        .new_joined_client("lnv2-lnurl-test-client-b")
500        .await?;
501
502    for (gw_send, gw_receive) in gateway_pairs {
503        info!(
504            "Testing lnurl payments: {} -> {} -> client",
505            gw_send.ln.ln_type(),
506            gw_receive.ln.ln_type()
507        );
508
509        let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
510        let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
511
512        let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
513        let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
514
515        let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
516        let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
517
518        let response_a = verify_payment(&verify_url_a).await?;
519        let response_b = verify_payment(&verify_url_b).await?;
520
521        assert!(!response_a.settled);
522        assert!(!response_b.settled);
523
524        assert!(response_a.preimage.is_none());
525        assert!(response_b.preimage.is_none());
526
527        gw_send.pay_invoice(invoice_a.clone()).await?;
528        gw_send.pay_invoice(invoice_b.clone()).await?;
529
530        let response_a = verify_payment(&verify_url_a).await?;
531        let response_b = verify_payment(&verify_url_b).await?;
532
533        assert!(response_a.settled);
534        assert!(response_b.settled);
535
536        verify_preimage(&response_a, &invoice_a);
537        verify_preimage(&response_b, &invoice_b);
538
539        assert_eq!(verify_task_a.await??, response_a);
540        assert_eq!(verify_task_b.await??, response_b);
541    }
542
543    while client_a.balance().await? < 950 * 1000 {
544        info!("Waiting for client A to receive funds via LNURL...");
545
546        cmd!(client_a, "dev", "wait", "1").out_json().await?;
547    }
548
549    info!("Client A successfully received funds via LNURL!");
550
551    while client_b.balance().await? < 950 * 1000 {
552        info!("Waiting for client B to receive funds via LNURL...");
553
554        cmd!(client_b, "dev", "wait", "1").out_json().await?;
555    }
556
557    info!("Client B successfully received funds via LNURL!");
558
559    Ok(())
560}
561
562async fn generate_lnurl(
563    client: &Client,
564    recurringd_base_url: &str,
565    gw_ldk_addr: &str,
566) -> anyhow::Result<String> {
567    cmd!(
568        client,
569        "module",
570        "lnv2",
571        "lnurl",
572        "generate",
573        recurringd_base_url,
574        "--gateway",
575        gw_ldk_addr,
576    )
577    .out_json()
578    .await
579    .map(|s| s.as_str().unwrap().to_owned())
580}
581
582fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
583    let preimage = response
584        .preimage
585        .as_ref()
586        .expect("Payment should be settled");
587
588    let payment_hash = hex_conservative::decode_to_array::<32>(preimage)
589        .expect("Valid hex")
590        .consensus_hash::<sha256::Hash>();
591
592    assert_eq!(payment_hash, *invoice.payment_hash());
593}
594
595async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
596    let response = reqwest::get(verify_url)
597        .await?
598        .json::<VerifyResponse>()
599        .await?;
600
601    Ok(response)
602}
603
604async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
605    let response = reqwest::get(format!("{verify_url}?wait"))
606        .await?
607        .json::<VerifyResponse>()
608        .await?;
609
610    Ok(response)
611}
612
613#[derive(Deserialize, Clone)]
614struct LnUrlPayResponse {
615    callback: String,
616}
617
618#[derive(Deserialize, Clone)]
619struct LnUrlPayInvoiceResponse {
620    pr: Bolt11Invoice,
621    verify: String,
622}
623
624async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
625    let endpoint = LnUrl::from_str(&lnurl)?;
626
627    let response = reqwest::get(endpoint.url)
628        .await?
629        .json::<LnUrlPayResponse>()
630        .await?;
631
632    let callback_url = format!("{}?amount={}", response.callback, amount_msat);
633
634    let response = reqwest::get(callback_url)
635        .await?
636        .json::<LnUrlPayInvoiceResponse>()
637        .await?;
638
639    ensure!(
640        response.pr.amount_milli_satoshis() == Some(amount_msat),
641        "Invoice amount is not set"
642    );
643
644    Ok((response.pr, response.verify))
645}
646
647async fn test_iroh_payment(
648    client: &Client,
649    gw_lnd: &Gatewayd,
650    gw_ldk: &Gatewayd,
651) -> anyhow::Result<()> {
652    info!("Testing iroh payment...");
653    add_gateway(client, 0, &format!("iroh://{}", gw_lnd.node_id)).await?;
654    add_gateway(client, 1, &format!("iroh://{}", gw_lnd.node_id)).await?;
655    add_gateway(client, 2, &format!("iroh://{}", gw_lnd.node_id)).await?;
656    add_gateway(client, 3, &format!("iroh://{}", gw_lnd.node_id)).await?;
657
658    // If the client is below v0.10.0, also add the HTTP address so that the client
659    // can fallback to using that, since the iroh gateway will fail.
660    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
661        || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
662    {
663        add_gateway(client, 0, &gw_lnd.addr).await?;
664        add_gateway(client, 1, &gw_lnd.addr).await?;
665        add_gateway(client, 2, &gw_lnd.addr).await?;
666        add_gateway(client, 3, &gw_lnd.addr).await?;
667    }
668
669    let invoice = gw_ldk.create_invoice(5_000_000).await?;
670
671    let send_op = serde_json::from_value::<OperationId>(
672        cmd!(client, "module", "lnv2", "send", invoice,)
673            .out_json()
674            .await?,
675    )?;
676
677    assert_eq!(
678        cmd!(
679            client,
680            "module",
681            "lnv2",
682            "await-send",
683            serde_json::to_string(&send_op)?.substring(1, 65)
684        )
685        .out_json()
686        .await?,
687        serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
688    );
689
690    let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
691        cmd!(client, "module", "lnv2", "receive", "5000000",)
692            .out_json()
693            .await?,
694    )?;
695
696    gw_ldk.pay_invoice(invoice).await?;
697    common::await_receive_claimed(client, receive_op).await?;
698
699    if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
700        || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
701    {
702        remove_gateway(client, 0, &gw_lnd.addr).await?;
703        remove_gateway(client, 1, &gw_lnd.addr).await?;
704        remove_gateway(client, 2, &gw_lnd.addr).await?;
705        remove_gateway(client, 3, &gw_lnd.addr).await?;
706    }
707
708    remove_gateway(client, 0, &format!("iroh://{}", gw_lnd.node_id)).await?;
709    remove_gateway(client, 1, &format!("iroh://{}", gw_lnd.node_id)).await?;
710    remove_gateway(client, 2, &format!("iroh://{}", gw_lnd.node_id)).await?;
711    remove_gateway(client, 3, &format!("iroh://{}", gw_lnd.node_id)).await?;
712
713    Ok(())
714}