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