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::{VERSION_0_9_0_ALPHA, VERSION_0_10_0_ALPHA};
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_lnv2_client::FinalSendOperationState;
16use fedimint_lnv2_common::lnurl::VerifyResponse;
17use lightning_invoice::Bolt11Invoice;
18use lnurl::lnurl::LnUrl;
19use serde::Deserialize;
20use substring::Substring;
21use tokio::try_join;
22use tracing::info;
23
24#[path = "common.rs"]
25mod common;
26
27#[derive(Parser)]
28#[command(name = "lnv2-module-tests")]
29#[command(about = "LNv2 module integration tests", long_about = None)]
30struct Cli {
31 #[command(subcommand)]
32 command: Option<Commands>,
33}
34
35#[derive(Subcommand)]
36enum Commands {
37 GatewayRegistration,
39 Payments,
41 LnurlPay,
43}
44
45#[tokio::main]
46async fn main() -> anyhow::Result<()> {
47 let cli = Cli::parse();
48
49 devimint::run_devfed_test()
50 .call(|dev_fed, _process_mgr| async move {
51 if !devimint::util::supports_lnv2() {
52 info!("lnv2 is disabled, skipping");
53 return Ok(());
54 }
55
56 if !devimint::util::is_backwards_compatibility_test() {
57 info!("Verifying that LNv1 module is disabled...");
58
59 ensure!(
60 !devimint::util::supports_lnv1(),
61 "LNv1 module should be disabled when not in backwards compatibility test"
62 );
63 }
64
65 match &cli.command {
66 Some(Commands::GatewayRegistration) => {
67 test_gateway_registration(&dev_fed).await?;
68 }
69 Some(Commands::Payments) => {
70 test_payments(&dev_fed).await?;
71 }
72 Some(Commands::LnurlPay) => {
73 pegin_gateways(&dev_fed).await?;
74 test_lnurl_pay(&dev_fed, false).await?;
75 test_lnurl_pay(&dev_fed, true).await?;
76 }
77 None => {
78 test_gateway_registration(&dev_fed).await?;
80 test_payments(&dev_fed).await?;
81 test_lnurl_pay(&dev_fed, false).await?;
82 test_lnurl_pay(&dev_fed, true).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, use_v2: bool) -> anyhow::Result<()> {
473 let min_version = if use_v2 {
474 &*VERSION_0_10_0_ALPHA
475 } else {
476 &*VERSION_0_9_0_ALPHA
477 };
478
479 if util::FedimintCli::version_or_default().await < *min_version {
480 return Ok(());
481 }
482
483 if util::FedimintdCmd::version_or_default().await < *min_version {
484 return Ok(());
485 }
486
487 if util::Gatewayd::version_or_default().await < *min_version {
488 return Ok(());
489 }
490
491 let federation = dev_fed.fed().await?;
492
493 let gw_lnd = dev_fed.gw_lnd().await?;
494 let gw_ldk = dev_fed.gw_ldk().await?;
495
496 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
497
498 let recurringd = if use_v2 {
499 dev_fed.recurringdv2().await?.api_url().to_string()
500 } else {
501 dev_fed.recurringd().await?.api_url().to_string()
502 };
503
504 let client_a = federation
505 .new_joined_client("lnv2-lnurl-test-client-a")
506 .await?;
507
508 let client_b = federation
509 .new_joined_client("lnv2-lnurl-test-client-b")
510 .await?;
511
512 for (gw_send, gw_receive) in gateway_pairs {
513 info!(
514 "Testing lnurl payments: {} -> {} -> client",
515 gw_send.ln.ln_type(),
516 gw_receive.ln.ln_type()
517 );
518
519 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
520 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
521
522 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
523 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
524
525 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
526 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
527
528 let response_a = verify_payment(&verify_url_a).await?;
529 let response_b = verify_payment(&verify_url_b).await?;
530
531 assert!(!response_a.settled);
532 assert!(!response_b.settled);
533
534 assert!(response_a.preimage.is_none());
535 assert!(response_b.preimage.is_none());
536
537 gw_send.pay_invoice(invoice_a.clone()).await?;
538 gw_send.pay_invoice(invoice_b.clone()).await?;
539
540 let response_a = verify_payment(&verify_url_a).await?;
541 let response_b = verify_payment(&verify_url_b).await?;
542
543 assert!(response_a.settled);
544 assert!(response_b.settled);
545
546 let payment_hash = response_a
547 .preimage
548 .expect("Payment A should be settled")
549 .consensus_hash::<sha256::Hash>();
550
551 assert_eq!(payment_hash, *invoice_a.payment_hash());
552
553 let payment_hash = response_b
554 .preimage
555 .expect("Payment B should be settled")
556 .consensus_hash::<sha256::Hash>();
557
558 assert_eq!(payment_hash, *invoice_b.payment_hash());
559
560 assert_eq!(verify_task_a.await??.preimage, response_a.preimage);
561 assert_eq!(verify_task_b.await??.preimage, response_b.preimage);
562 }
563
564 while client_a.balance().await? < 950 * 1000 {
565 info!("Waiting for client A to receive funds via LNURL...");
566
567 cmd!(client_a, "dev", "wait", "1").out_json().await?;
568 }
569
570 info!("Client A successfully received funds via LNURL!");
571
572 while client_b.balance().await? < 950 * 1000 {
573 info!("Waiting for client B to receive funds via LNURL...");
574
575 cmd!(client_b, "dev", "wait", "1").out_json().await?;
576 }
577
578 info!("Client B successfully received funds via LNURL!");
579
580 Ok(())
581}
582
583async fn generate_lnurl(
584 client: &Client,
585 recurringd_base_url: &str,
586 gw_ldk_addr: &str,
587) -> anyhow::Result<String> {
588 cmd!(
589 client,
590 "module",
591 "lnv2",
592 "lnurl",
593 "generate",
594 recurringd_base_url,
595 "--gateway",
596 gw_ldk_addr,
597 )
598 .out_json()
599 .await
600 .map(|s| s.as_str().unwrap().to_owned())
601}
602
603async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
604 let response = reqwest::get(verify_url)
605 .await?
606 .json::<VerifyResponse>()
607 .await?;
608
609 Ok(response)
610}
611
612async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
613 let response = reqwest::get(format!("{verify_url}?wait"))
614 .await?
615 .json::<VerifyResponse>()
616 .await?;
617
618 Ok(response)
619}
620
621#[derive(Deserialize, Clone)]
622struct LnUrlPayResponse {
623 callback: String,
624}
625
626#[derive(Deserialize, Clone)]
627struct LnUrlPayInvoiceResponse {
628 pr: Bolt11Invoice,
629 verify: String,
630}
631
632async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
633 let endpoint = LnUrl::from_str(&lnurl)?;
634
635 let response = reqwest::get(endpoint.url)
636 .await?
637 .json::<LnUrlPayResponse>()
638 .await?;
639
640 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
641
642 let response = reqwest::get(callback_url)
643 .await?
644 .json::<LnUrlPayInvoiceResponse>()
645 .await?;
646
647 ensure!(
648 response.pr.amount_milli_satoshis() == Some(amount_msat),
649 "Invoice amount is not set"
650 );
651
652 Ok((response.pr, response.verify))
653}
654
655async fn test_iroh_payment(
656 client: &Client,
657 gw_lnd: &Gatewayd,
658 gw_ldk: &Gatewayd,
659) -> anyhow::Result<()> {
660 info!("Testing iroh payment...");
661 add_gateway(client, 0, &format!("iroh://{}", gw_lnd.node_id)).await?;
662 add_gateway(client, 1, &format!("iroh://{}", gw_lnd.node_id)).await?;
663 add_gateway(client, 2, &format!("iroh://{}", gw_lnd.node_id)).await?;
664 add_gateway(client, 3, &format!("iroh://{}", gw_lnd.node_id)).await?;
665
666 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
669 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
670 {
671 add_gateway(client, 0, &gw_lnd.addr).await?;
672 add_gateway(client, 1, &gw_lnd.addr).await?;
673 add_gateway(client, 2, &gw_lnd.addr).await?;
674 add_gateway(client, 3, &gw_lnd.addr).await?;
675 }
676
677 let invoice = gw_ldk.create_invoice(5_000_000).await?;
678
679 let send_op = serde_json::from_value::<OperationId>(
680 cmd!(client, "module", "lnv2", "send", invoice,)
681 .out_json()
682 .await?,
683 )?;
684
685 assert_eq!(
686 cmd!(
687 client,
688 "module",
689 "lnv2",
690 "await-send",
691 serde_json::to_string(&send_op)?.substring(1, 65)
692 )
693 .out_json()
694 .await?,
695 serde_json::to_value(FinalSendOperationState::Success).expect("JSON serialization failed"),
696 );
697
698 let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
699 cmd!(client, "module", "lnv2", "receive", "5000000",)
700 .out_json()
701 .await?,
702 )?;
703
704 gw_ldk.pay_invoice(invoice).await?;
705 common::await_receive_claimed(client, receive_op).await?;
706
707 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
708 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
709 {
710 remove_gateway(client, 0, &gw_lnd.addr).await?;
711 remove_gateway(client, 1, &gw_lnd.addr).await?;
712 remove_gateway(client, 2, &gw_lnd.addr).await?;
713 remove_gateway(client, 3, &gw_lnd.addr).await?;
714 }
715
716 remove_gateway(client, 0, &format!("iroh://{}", gw_lnd.node_id)).await?;
717 remove_gateway(client, 1, &format!("iroh://{}", gw_lnd.node_id)).await?;
718 remove_gateway(client, 2, &format!("iroh://{}", gw_lnd.node_id)).await?;
719 remove_gateway(client, 3, &format!("iroh://{}", gw_lnd.node_id)).await?;
720
721 Ok(())
722}