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_9_0_ALPHA;
9use devimint::{Gatewayd, cmd, util};
10use fedimint_core::core::OperationId;
11use fedimint_core::encoding::Encodable;
12use fedimint_core::task::{self};
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.clone();
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    info!("Testing payment summary...");
333
334    let lnd_payment_summary = gw_lnd.payment_summary().await?;
335
336    assert_eq!(
337        lnd_payment_summary.outgoing.total_success,
338        4 + lnv1_swap + lnv2_swap
339    );
340    assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
341    // LNv1 does not count swaps as incoming payments
342    assert_eq!(lnd_payment_summary.incoming.total_success, 3 + lnv1_swap);
343    assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
344
345    assert!(lnd_payment_summary.outgoing.median_latency.is_some());
346    assert!(lnd_payment_summary.outgoing.average_latency.is_some());
347    assert!(lnd_payment_summary.incoming.median_latency.is_some());
348    assert!(lnd_payment_summary.incoming.average_latency.is_some());
349
350    let ldk_payment_summary = gw_ldk.payment_summary().await?;
351
352    assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
353    assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
354    assert_eq!(ldk_payment_summary.incoming.total_success, 4);
355    assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
356
357    assert!(ldk_payment_summary.outgoing.median_latency.is_some());
358    assert!(ldk_payment_summary.outgoing.average_latency.is_some());
359    assert!(ldk_payment_summary.incoming.median_latency.is_some());
360    assert!(ldk_payment_summary.incoming.average_latency.is_some());
361
362    Ok(())
363}
364
365async fn test_fees(
366    fed_id: String,
367    client: &Client,
368    gw_lnd: &Gatewayd,
369    gw_ldk: &Gatewayd,
370    expected_addition: u64,
371) -> anyhow::Result<()> {
372    let gw_lnd_ecash_prev = gw_lnd.ecash_balance(fed_id.clone()).await?;
373
374    let (invoice, receive_op) = receive(client, &gw_ldk.addr, 1_000_000).await?;
375
376    test_send(
377        client,
378        &gw_lnd.addr,
379        &invoice.to_string(),
380        FinalSendOperationState::Success,
381    )
382    .await?;
383
384    await_receive_claimed(client, receive_op).await?;
385
386    let gw_lnd_ecash_after = gw_lnd.ecash_balance(fed_id.clone()).await?;
387
388    almost_equal(
389        gw_lnd_ecash_prev + expected_addition,
390        gw_lnd_ecash_after,
391        5000,
392    )
393    .unwrap();
394
395    Ok(())
396}
397
398async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
399    cmd!(
400        client,
401        "--our-id",
402        peer.to_string(),
403        "--password",
404        "pass",
405        "module",
406        "lnv2",
407        "gateways",
408        "add",
409        gateway
410    )
411    .out_json()
412    .await?
413    .as_bool()
414    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
415}
416
417async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
418    cmd!(
419        client,
420        "--our-id",
421        peer.to_string(),
422        "--password",
423        "pass",
424        "module",
425        "lnv2",
426        "gateways",
427        "remove",
428        gateway
429    )
430    .out_json()
431    .await?
432    .as_bool()
433    .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
434}
435
436async fn receive(
437    client: &Client,
438    gateway: &str,
439    amount: u64,
440) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
441    Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
442        cmd!(
443            client,
444            "module",
445            "lnv2",
446            "receive",
447            amount,
448            "--gateway",
449            gateway
450        )
451        .out_json()
452        .await?,
453    )?)
454}
455
456async fn test_send(
457    client: &Client,
458    gateway: &String,
459    invoice: &String,
460    final_state: FinalSendOperationState,
461) -> anyhow::Result<()> {
462    let send_op = serde_json::from_value::<OperationId>(
463        cmd!(
464            client,
465            "module",
466            "lnv2",
467            "send",
468            invoice,
469            "--gateway",
470            gateway
471        )
472        .out_json()
473        .await?,
474    )?;
475
476    assert_eq!(
477        cmd!(
478            client,
479            "module",
480            "lnv2",
481            "await-send",
482            serde_json::to_string(&send_op)?.substring(1, 65)
483        )
484        .out_json()
485        .await?,
486        serde_json::to_value(final_state).expect("JSON serialization failed"),
487    );
488
489    Ok(())
490}
491
492async fn receive_lnv1(
493    client: &Client,
494    gateway_id: &String,
495    amount_msats: u64,
496) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
497    let invoice_response = cmd!(
498        client,
499        "module",
500        "ln",
501        "invoice",
502        amount_msats,
503        "--gateway-id",
504        gateway_id
505    )
506    .out_json()
507    .await?;
508    let invoice = serde_json::from_value::<Bolt11Invoice>(
509        invoice_response
510            .get("invoice")
511            .expect("Invoice should be present")
512            .clone(),
513    )?;
514    let operation_id = serde_json::from_value::<OperationId>(
515        invoice_response
516            .get("operation_id")
517            .expect("OperationId should be present")
518            .clone(),
519    )?;
520    Ok((invoice, operation_id))
521}
522
523async fn test_send_lnv1(client: &Client, gateway_id: &str, invoice: &str) -> anyhow::Result<()> {
524    let payment_result = cmd!(
525        client,
526        "module",
527        "ln",
528        "pay",
529        invoice,
530        "--gateway-id",
531        gateway_id
532    )
533    .out_json()
534    .await?;
535    assert!(payment_result.get("Success").is_some() || payment_result.get("preimage").is_some());
536    Ok(())
537}
538
539async fn await_receive_claimed(client: &Client, operation_id: OperationId) -> anyhow::Result<()> {
540    assert_eq!(
541        cmd!(
542            client,
543            "module",
544            "lnv2",
545            "await-receive",
546            serde_json::to_string(&operation_id)?.substring(1, 65)
547        )
548        .out_json()
549        .await?,
550        serde_json::to_value(FinalReceiveOperationState::Claimed)
551            .expect("JSON serialization failed"),
552    );
553
554    Ok(())
555}
556
557async fn await_receive_lnv1(client: &Client, operation_id: OperationId) -> anyhow::Result<()> {
558    let lnv1_response = cmd!(client, "await-invoice", operation_id.fmt_full())
559        .out_json()
560        .await?;
561    assert!(lnv1_response.get("total_amount_msat").is_some());
562    Ok(())
563}
564async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
565    if util::FedimintCli::version_or_default().await < *VERSION_0_9_0_ALPHA {
566        return Ok(());
567    }
568
569    if util::FedimintdCmd::version_or_default().await < *VERSION_0_9_0_ALPHA {
570        return Ok(());
571    }
572
573    if util::Gatewayd::version_or_default().await < *VERSION_0_9_0_ALPHA {
574        return Ok(());
575    }
576
577    info!("Testing LNURL pay...");
578
579    let federation = dev_fed.fed().await?;
580
581    let gw_lnd = dev_fed.gw_lnd().await?;
582    let gw_ldk = dev_fed.gw_ldk().await?;
583
584    let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
585
586    let recurringd = dev_fed.recurringd().await?.api_url().to_string();
587
588    let client_a = federation
589        .new_joined_client("lnv2-lnurl-test-client-a")
590        .await?;
591
592    let client_b = federation
593        .new_joined_client("lnv2-lnurl-test-client-b")
594        .await?;
595
596    for (gw_send, gw_receive) in gateway_pairs {
597        info!(
598            "Testing lnurl payments: {} -> {} -> client",
599            gw_send.ln.ln_type(),
600            gw_receive.ln.ln_type()
601        );
602
603        let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
604        let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
605
606        let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
607        let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
608
609        let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
610        let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
611
612        let response_a = verify_payment(&verify_url_a).await?;
613        let response_b = verify_payment(&verify_url_b).await?;
614
615        assert!(!response_a.settled);
616        assert!(!response_b.settled);
617
618        assert!(response_a.preimage.is_none());
619        assert!(response_b.preimage.is_none());
620
621        gw_send.pay_invoice(invoice_a.clone()).await?;
622        gw_send.pay_invoice(invoice_b.clone()).await?;
623
624        let response_a = verify_payment(&verify_url_a).await?;
625        let response_b = verify_payment(&verify_url_b).await?;
626
627        assert!(response_a.settled);
628        assert!(response_b.settled);
629
630        let payment_hash = response_a
631            .preimage
632            .expect("Payment A should be settled")
633            .consensus_hash::<sha256::Hash>();
634
635        assert_eq!(payment_hash, *invoice_a.payment_hash());
636
637        let payment_hash = response_b
638            .preimage
639            .expect("Payment B should be settled")
640            .consensus_hash::<sha256::Hash>();
641
642        assert_eq!(payment_hash, *invoice_b.payment_hash());
643
644        assert_eq!(verify_task_a.await??.preimage, response_a.preimage);
645        assert_eq!(verify_task_b.await??.preimage, response_b.preimage);
646    }
647
648    while client_a.balance().await? < 950 * 1000 {
649        info!("Waiting for client A to receive funds via LNURL...");
650
651        cmd!(client_a, "dev", "wait", "1").out_json().await?;
652    }
653
654    info!("Client A successfully received funds via LNURL!");
655
656    while client_b.balance().await? < 950 * 1000 {
657        info!("Waiting for client B to receive funds via LNURL...");
658
659        cmd!(client_b, "dev", "wait", "1").out_json().await?;
660    }
661
662    info!("Client B successfully received funds via LNURL!");
663
664    Ok(())
665}
666
667async fn generate_lnurl(
668    client: &Client,
669    recurringd_base_url: &str,
670    gw_ldk_addr: &str,
671) -> anyhow::Result<String> {
672    cmd!(
673        client,
674        "module",
675        "lnv2",
676        "lnurl",
677        "generate",
678        recurringd_base_url,
679        "--gateway",
680        gw_ldk_addr,
681    )
682    .out_json()
683    .await
684    .map(|s| s.as_str().unwrap().to_owned())
685}
686
687async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
688    let response = reqwest::get(verify_url)
689        .await?
690        .json::<VerifyResponse>()
691        .await?;
692
693    Ok(response)
694}
695
696async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
697    let response = reqwest::get(format!("{verify_url}?wait"))
698        .await?
699        .json::<VerifyResponse>()
700        .await?;
701
702    Ok(response)
703}
704
705#[derive(Deserialize, Clone)]
706struct LnUrlPayResponse {
707    callback: String,
708}
709
710#[derive(Deserialize, Clone)]
711struct LnUrlPayInvoiceResponse {
712    pr: Bolt11Invoice,
713    verify: String,
714}
715
716async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
717    let endpoint = LnUrl::from_str(&lnurl)?;
718
719    let response = reqwest::get(endpoint.url)
720        .await?
721        .json::<LnUrlPayResponse>()
722        .await?;
723
724    let callback_url = format!("{}?amount={}", response.callback, amount_msat);
725
726    let response = reqwest::get(callback_url)
727        .await?
728        .json::<LnUrlPayInvoiceResponse>()
729        .await?;
730
731    ensure!(
732        response.pr.amount_milli_satoshis() == Some(amount_msat),
733        "Invoice amount is not set"
734    );
735
736    Ok((response.pr, response.verify))
737}