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