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