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 gatewayd_version = crate::util::Gatewayd::version_or_default().await;
228    let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
229    if gatewayd_version >= *VERSION_0_9_0_ALPHA && gateway_cli_version >= *VERSION_0_9_0_ALPHA {
230        info!("Testing LNv1 client can pay LNv2 invoice...");
231        let lnd_gw_id = gw_lnd.gateway_id().await?;
232        let (invoice, receive_op) = receive(&client, &gw_lnd.addr, 1_000_000).await?;
233        test_send_lnv1(&client, &lnd_gw_id, &invoice.to_string()).await?;
234        lnv1_swap += 1;
235        await_receive_claimed(&client, receive_op).await?;
236    }
237
238    info!("Testing payments from client to gateways...");
239
240    for (gw_send, gw_receive) in gateway_pairs {
241        info!(
242            "Testing payment: client -> {} -> {}",
243            gw_send.ln.ln_type(),
244            gw_receive.ln.ln_type()
245        );
246
247        let invoice = gw_receive.create_invoice(1_000_000).await?;
248
249        test_send(
250            &client,
251            &gw_send.addr,
252            &invoice.to_string(),
253            FinalSendOperationState::Success,
254        )
255        .await?;
256    }
257
258    info!("Testing payments from gateways to client...");
259
260    for (gw_send, gw_receive) in gateway_pairs {
261        info!(
262            "Testing payment: {} -> {} -> client",
263            gw_send.ln.ln_type(),
264            gw_receive.ln.ln_type()
265        );
266
267        let (invoice, receive_op) = receive(&client, &gw_receive.addr, 1_000_000).await?;
268
269        gw_send.pay_invoice(invoice).await?;
270
271        await_receive_claimed(&client, receive_op).await?;
272    }
273
274    retry(
275        "Waiting for the full balance to become available to the client".to_string(),
276        backoff_util::background_backoff(),
277        || async {
278            ensure!(client.balance().await? >= 9000 * 1000);
279
280            Ok(())
281        },
282    )
283    .await?;
284
285    info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
286
287    try_join!(
288        test_send(
289            &client,
290            &gw_ldk.addr,
291            &hold_invoice,
292            FinalSendOperationState::Success
293        ),
294        lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
295    )?;
296
297    info!("Testing LNv2 lightning fees...");
298
299    let fed_id = federation.calculate_federation_id();
300
301    gw_lnd
302        .set_federation_routing_fee(fed_id.clone(), 0, 0)
303        .await?;
304
305    gw_lnd
306        .set_federation_transaction_fee(fed_id.clone(), 0, 0)
307        .await?;
308
309    if util::FedimintdCmd::version_or_default().await >= *VERSION_0_9_0_ALPHA {
310        // Gateway pays: 1_000 msat LNv2 federation base fee. Gateway receives:
311        // 1_000_000 payment.
312        test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
313    } else {
314        // Gateway pays: 1_000 msat LNv2 federation base fee, 100 msat LNv2 federation
315        // relative fee. Gateway receives: 1_000_000 payment.
316        test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000 - 100).await?;
317    }
318
319    let gatewayd_version = util::Gatewayd::version_or_default().await;
320
321    if gatewayd_version >= *VERSION_0_7_0_ALPHA {
322        info!("Testing payment summary...");
323
324        let lnd_payment_summary = gw_lnd.payment_summary().await?;
325
326        assert_eq!(lnd_payment_summary.outgoing.total_success, 4 + lnv1_swap);
327        assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
328        assert_eq!(lnd_payment_summary.incoming.total_success, 3 + lnv1_swap);
329        assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
330
331        assert!(lnd_payment_summary.outgoing.median_latency.is_some());
332        assert!(lnd_payment_summary.outgoing.average_latency.is_some());
333        assert!(lnd_payment_summary.incoming.median_latency.is_some());
334        assert!(lnd_payment_summary.incoming.average_latency.is_some());
335
336        let ldk_payment_summary = gw_ldk.payment_summary().await?;
337
338        assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
339        assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
340        assert_eq!(ldk_payment_summary.incoming.total_success, 4);
341        assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
342
343        assert!(ldk_payment_summary.outgoing.median_latency.is_some());
344        assert!(ldk_payment_summary.outgoing.average_latency.is_some());
345        assert!(ldk_payment_summary.incoming.median_latency.is_some());
346        assert!(ldk_payment_summary.incoming.average_latency.is_some());
347    }
348
349    Ok(())
350}
351
352async fn test_fees(
353    fed_id: String,
354    client: &Client,
355    gw_lnd: &Gatewayd,
356    gw_ldk: &Gatewayd,
357    expected_addition: u64,
358) -> anyhow::Result<()> {
359    let gw_lnd_ecash_prev = gw_lnd.ecash_balance(fed_id.clone()).await?;
360
361    let (invoice, receive_op) = receive(client, &gw_ldk.addr, 1_000_000).await?;
362
363    test_send(
364        client,
365        &gw_lnd.addr,
366        &invoice.to_string(),
367        FinalSendOperationState::Success,
368    )
369    .await?;
370
371    await_receive_claimed(client, receive_op).await?;
372
373    let gw_lnd_ecash_after = gw_lnd.ecash_balance(fed_id.clone()).await?;
374
375    almost_equal(
376        gw_lnd_ecash_prev + expected_addition,
377        gw_lnd_ecash_after,
378        5000,
379    )
380    .unwrap();
381
382    Ok(())
383}
384
385async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
386    cmd!(
387        client,
388        "--our-id",
389        peer.to_string(),
390        "--password",
391        "pass",
392        "module",
393        "lnv2",
394        "gateways",
395        "add",
396        gateway
397    )
398    .out_json()
399    .await?
400    .as_bool()
401    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
402}
403
404async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
405    cmd!(
406        client,
407        "--our-id",
408        peer.to_string(),
409        "--password",
410        "pass",
411        "module",
412        "lnv2",
413        "gateways",
414        "remove",
415        gateway
416    )
417    .out_json()
418    .await?
419    .as_bool()
420    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
421}
422
423async fn receive(
424    client: &Client,
425    gateway: &str,
426    amount: u64,
427) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
428    Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
429        cmd!(
430            client,
431            "module",
432            "lnv2",
433            "receive",
434            amount,
435            "--gateway",
436            gateway
437        )
438        .out_json()
439        .await?,
440    )?)
441}
442
443async fn test_send(
444    client: &Client,
445    gateway: &String,
446    invoice: &String,
447    final_state: FinalSendOperationState,
448) -> anyhow::Result<()> {
449    let send_op = serde_json::from_value::<OperationId>(
450        cmd!(
451            client,
452            "module",
453            "lnv2",
454            "send",
455            invoice,
456            "--gateway",
457            gateway
458        )
459        .out_json()
460        .await?,
461    )?;
462
463    assert_eq!(
464        cmd!(
465            client,
466            "module",
467            "lnv2",
468            "await-send",
469            serde_json::to_string(&send_op)?.substring(1, 65)
470        )
471        .out_json()
472        .await?,
473        serde_json::to_value(final_state).expect("JSON serialization failed"),
474    );
475
476    Ok(())
477}
478
479async fn test_send_lnv1(
480    client: &Client,
481    gateway_id: &String,
482    invoice: &String,
483) -> anyhow::Result<()> {
484    let payment_result = cmd!(
485        client,
486        "module",
487        "ln",
488        "pay",
489        invoice,
490        "--gateway-id",
491        gateway_id
492    )
493    .out_json()
494    .await?;
495    assert!(payment_result.get("Success").is_some() || payment_result.get("preimage").is_some());
496    Ok(())
497}
498
499async fn await_receive_claimed(client: &Client, operation_id: OperationId) -> anyhow::Result<()> {
500    assert_eq!(
501        cmd!(
502            client,
503            "module",
504            "lnv2",
505            "await-receive",
506            serde_json::to_string(&operation_id)?.substring(1, 65)
507        )
508        .out_json()
509        .await?,
510        serde_json::to_value(FinalReceiveOperationState::Claimed)
511            .expect("JSON serialization failed"),
512    );
513
514    Ok(())
515}
516
517#[allow(dead_code)]
518async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
519    if util::FedimintCli::version_or_default().await < *VERSION_0_9_0_ALPHA {
520        return Ok(());
521    }
522
523    if util::FedimintdCmd::version_or_default().await < *VERSION_0_9_0_ALPHA {
524        return Ok(());
525    }
526
527    if util::Gatewayd::version_or_default().await < *VERSION_0_9_0_ALPHA {
528        return Ok(());
529    }
530
531    info!("Testing LNURL pay...");
532
533    let federation = dev_fed.fed().await?;
534
535    let gw_lnd = dev_fed.gw_lnd().await?;
536    let gw_ldk = dev_fed.gw_ldk().await?;
537
538    let gateway_matrix = [
539        (gw_lnd, gw_lnd),
540        (gw_lnd, gw_ldk),
541        (gw_ldk, gw_lnd),
542        (gw_ldk, gw_ldk),
543    ];
544
545    let recurringd = dev_fed.recurringd().await?.api_url().to_string();
546
547    let client_a = federation
548        .new_joined_client("lnv2-lnurl-test-client-a")
549        .await?;
550
551    let client_b = federation
552        .new_joined_client("lnv2-lnurl-test-client-b")
553        .await?;
554
555    federation.pegin_client(10_000, &client_a).await?;
556    federation.pegin_client(10_000, &client_b).await?;
557
558    assert_eq!(client_a.balance().await?, 10_000 * 1000);
559    assert_eq!(client_b.balance().await?, 10_000 * 1000);
560
561    for (gw_send, gw_receive) in gateway_matrix {
562        info!(
563            "Testing lnurl payments: client -> {} -> {} -> client",
564            gw_send.ln.ln_type(),
565            gw_receive.ln.ln_type()
566        );
567
568        // Generate LNURL using LNv2 client command
569        let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
570        let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
571
572        let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
573        let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
574
575        let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
576        let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
577
578        let response_a = verify_payment(&verify_url_a).await?;
579        let response_b = verify_payment(&verify_url_b).await?;
580
581        assert!(!response_a.settled);
582        assert!(!response_b.settled);
583
584        assert!(response_a.preimage.is_none());
585        assert!(response_b.preimage.is_none());
586
587        test_send(
588            &client_a,
589            &gw_send.addr,
590            &invoice_b.to_string(),
591            FinalSendOperationState::Success,
592        )
593        .await?;
594
595        test_send(
596            &client_b,
597            &gw_send.addr,
598            &invoice_a.to_string(),
599            FinalSendOperationState::Success,
600        )
601        .await?;
602
603        let response_a = verify_payment(&verify_url_a).await?;
604        let response_b = verify_payment(&verify_url_b).await?;
605
606        assert!(response_a.settled);
607        assert!(response_b.settled);
608
609        let payment_hash = response_a
610            .preimage
611            .expect("Payment A should be settled")
612            .consensus_hash::<sha256::Hash>();
613
614        assert_eq!(payment_hash, *invoice_a.payment_hash());
615
616        let payment_hash = response_b
617            .preimage
618            .expect("Payment B should be settled")
619            .consensus_hash::<sha256::Hash>();
620
621        assert_eq!(payment_hash, *invoice_b.payment_hash());
622
623        assert_eq!(verify_task_a.await??.preimage, response_a.preimage);
624        assert_eq!(verify_task_b.await??.preimage, response_b.preimage);
625    }
626
627    assert_eq!(receive_lnurl(&client_a).await?, 1);
628    assert_eq!(receive_lnurl(&client_a).await?, 2);
629    assert_eq!(receive_lnurl(&client_a).await?, 1);
630
631    assert_eq!(receive_lnurl(&client_b).await?, 3);
632    assert_eq!(receive_lnurl(&client_b).await?, 1);
633
634    Ok(())
635}
636
637async fn receive_lnurl(client: &Client) -> anyhow::Result<u64> {
638    cmd!(
639        client,
640        "module",
641        "lnv2",
642        "lnurl",
643        "receive",
644        "--batch-size",
645        "3"
646    )
647    .out_string()
648    .await
649    .map(|s| s.parse::<u64>().unwrap())
650}
651
652async fn generate_lnurl(
653    client: &Client,
654    recurringd_base_url: &str,
655    gw_ldk_addr: &str,
656) -> anyhow::Result<String> {
657    cmd!(
658        client,
659        "module",
660        "lnv2",
661        "lnurl",
662        "generate",
663        recurringd_base_url,
664        "--gateway",
665        gw_ldk_addr,
666    )
667    .out_json()
668    .await
669    .map(|s| s.as_str().unwrap().to_owned())
670}
671
672async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
673    let response = reqwest::get(verify_url)
674        .await?
675        .json::<VerifyResponse>()
676        .await?;
677
678    Ok(response)
679}
680
681async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
682    let response = reqwest::get(format!("{verify_url}?wait"))
683        .await?
684        .json::<VerifyResponse>()
685        .await?;
686
687    Ok(response)
688}
689
690#[derive(Deserialize, Clone)]
691struct LnUrlPayResponse {
692    callback: String,
693}
694
695#[derive(Deserialize, Clone)]
696struct LnUrlPayInvoiceResponse {
697    pr: Bolt11Invoice,
698    verify: String,
699}
700
701async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
702    let endpoint = LnUrl::from_str(&lnurl)?;
703
704    let response = reqwest::get(endpoint.url)
705        .await?
706        .json::<LnUrlPayResponse>()
707        .await?;
708
709    let callback_url = format!("{}?amount={}", response.callback, amount_msat);
710
711    let response = reqwest::get(callback_url)
712        .await?
713        .json::<LnUrlPayInvoiceResponse>()
714        .await?;
715
716    ensure!(
717        response.pr.amount_milli_satoshis() == Some(amount_msat),
718        "Invoice amount is not set"
719    );
720
721    Ok((response.pr, response.verify))
722}