lnv2_module_tests/
tests.rs

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