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