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