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