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 gatewayd_version = crate::util::Gatewayd::version_or_default().await;
228 let gateway_cli_version = crate::util::GatewayCli::version_or_default().await;
229 if gatewayd_version >= *VERSION_0_9_0_ALPHA && gateway_cli_version >= *VERSION_0_9_0_ALPHA {
230 info!("Testing LNv1 client can pay LNv2 invoice...");
231 let lnd_gw_id = gw_lnd.gateway_id().await?;
232 let (invoice, receive_op) = receive(&client, &gw_lnd.addr, 1_000_000).await?;
233 test_send_lnv1(&client, &lnd_gw_id, &invoice.to_string()).await?;
234 lnv1_swap += 1;
235 await_receive_claimed(&client, receive_op).await?;
236 }
237
238 info!("Testing payments from client to gateways...");
239
240 for (gw_send, gw_receive) in gateway_pairs {
241 info!(
242 "Testing payment: client -> {} -> {}",
243 gw_send.ln.ln_type(),
244 gw_receive.ln.ln_type()
245 );
246
247 let invoice = gw_receive.create_invoice(1_000_000).await?;
248
249 test_send(
250 &client,
251 &gw_send.addr,
252 &invoice.to_string(),
253 FinalSendOperationState::Success,
254 )
255 .await?;
256 }
257
258 info!("Testing payments from gateways to client...");
259
260 for (gw_send, gw_receive) in gateway_pairs {
261 info!(
262 "Testing payment: {} -> {} -> client",
263 gw_send.ln.ln_type(),
264 gw_receive.ln.ln_type()
265 );
266
267 let (invoice, receive_op) = receive(&client, &gw_receive.addr, 1_000_000).await?;
268
269 gw_send.pay_invoice(invoice).await?;
270
271 await_receive_claimed(&client, receive_op).await?;
272 }
273
274 retry(
275 "Waiting for the full balance to become available to the client".to_string(),
276 backoff_util::background_backoff(),
277 || async {
278 ensure!(client.balance().await? >= 9000 * 1000);
279
280 Ok(())
281 },
282 )
283 .await?;
284
285 info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
286
287 try_join!(
288 test_send(
289 &client,
290 &gw_ldk.addr,
291 &hold_invoice,
292 FinalSendOperationState::Success
293 ),
294 lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
295 )?;
296
297 info!("Testing LNv2 lightning fees...");
298
299 let fed_id = federation.calculate_federation_id();
300
301 gw_lnd
302 .set_federation_routing_fee(fed_id.clone(), 0, 0)
303 .await?;
304
305 gw_lnd
306 .set_federation_transaction_fee(fed_id.clone(), 0, 0)
307 .await?;
308
309 if util::FedimintdCmd::version_or_default().await >= *VERSION_0_9_0_ALPHA {
310 test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
313 } else {
314 test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000 - 100).await?;
317 }
318
319 let gatewayd_version = util::Gatewayd::version_or_default().await;
320
321 if gatewayd_version >= *VERSION_0_7_0_ALPHA {
322 info!("Testing payment summary...");
323
324 let lnd_payment_summary = gw_lnd.payment_summary().await?;
325
326 assert_eq!(lnd_payment_summary.outgoing.total_success, 4 + lnv1_swap);
327 assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
328 assert_eq!(lnd_payment_summary.incoming.total_success, 3 + lnv1_swap);
329 assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
330
331 assert!(lnd_payment_summary.outgoing.median_latency.is_some());
332 assert!(lnd_payment_summary.outgoing.average_latency.is_some());
333 assert!(lnd_payment_summary.incoming.median_latency.is_some());
334 assert!(lnd_payment_summary.incoming.average_latency.is_some());
335
336 let ldk_payment_summary = gw_ldk.payment_summary().await?;
337
338 assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
339 assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
340 assert_eq!(ldk_payment_summary.incoming.total_success, 4);
341 assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
342
343 assert!(ldk_payment_summary.outgoing.median_latency.is_some());
344 assert!(ldk_payment_summary.outgoing.average_latency.is_some());
345 assert!(ldk_payment_summary.incoming.median_latency.is_some());
346 assert!(ldk_payment_summary.incoming.average_latency.is_some());
347 }
348
349 Ok(())
350}
351
352async fn test_fees(
353 fed_id: String,
354 client: &Client,
355 gw_lnd: &Gatewayd,
356 gw_ldk: &Gatewayd,
357 expected_addition: u64,
358) -> anyhow::Result<()> {
359 let gw_lnd_ecash_prev = gw_lnd.ecash_balance(fed_id.clone()).await?;
360
361 let (invoice, receive_op) = receive(client, &gw_ldk.addr, 1_000_000).await?;
362
363 test_send(
364 client,
365 &gw_lnd.addr,
366 &invoice.to_string(),
367 FinalSendOperationState::Success,
368 )
369 .await?;
370
371 await_receive_claimed(client, receive_op).await?;
372
373 let gw_lnd_ecash_after = gw_lnd.ecash_balance(fed_id.clone()).await?;
374
375 almost_equal(
376 gw_lnd_ecash_prev + expected_addition,
377 gw_lnd_ecash_after,
378 5000,
379 )
380 .unwrap();
381
382 Ok(())
383}
384
385async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
386 cmd!(
387 client,
388 "--our-id",
389 peer.to_string(),
390 "--password",
391 "pass",
392 "module",
393 "lnv2",
394 "gateways",
395 "add",
396 gateway
397 )
398 .out_json()
399 .await?
400 .as_bool()
401 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
402}
403
404async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
405 cmd!(
406 client,
407 "--our-id",
408 peer.to_string(),
409 "--password",
410 "pass",
411 "module",
412 "lnv2",
413 "gateways",
414 "remove",
415 gateway
416 )
417 .out_json()
418 .await?
419 .as_bool()
420 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
421}
422
423async fn receive(
424 client: &Client,
425 gateway: &str,
426 amount: u64,
427) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
428 Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
429 cmd!(
430 client,
431 "module",
432 "lnv2",
433 "receive",
434 amount,
435 "--gateway",
436 gateway
437 )
438 .out_json()
439 .await?,
440 )?)
441}
442
443async fn test_send(
444 client: &Client,
445 gateway: &String,
446 invoice: &String,
447 final_state: FinalSendOperationState,
448) -> anyhow::Result<()> {
449 let send_op = serde_json::from_value::<OperationId>(
450 cmd!(
451 client,
452 "module",
453 "lnv2",
454 "send",
455 invoice,
456 "--gateway",
457 gateway
458 )
459 .out_json()
460 .await?,
461 )?;
462
463 assert_eq!(
464 cmd!(
465 client,
466 "module",
467 "lnv2",
468 "await-send",
469 serde_json::to_string(&send_op)?.substring(1, 65)
470 )
471 .out_json()
472 .await?,
473 serde_json::to_value(final_state).expect("JSON serialization failed"),
474 );
475
476 Ok(())
477}
478
479async fn test_send_lnv1(
480 client: &Client,
481 gateway_id: &String,
482 invoice: &String,
483) -> anyhow::Result<()> {
484 let payment_result = cmd!(
485 client,
486 "module",
487 "ln",
488 "pay",
489 invoice,
490 "--gateway-id",
491 gateway_id
492 )
493 .out_json()
494 .await?;
495 assert!(payment_result.get("Success").is_some() || payment_result.get("preimage").is_some());
496 Ok(())
497}
498
499async fn await_receive_claimed(client: &Client, operation_id: OperationId) -> anyhow::Result<()> {
500 assert_eq!(
501 cmd!(
502 client,
503 "module",
504 "lnv2",
505 "await-receive",
506 serde_json::to_string(&operation_id)?.substring(1, 65)
507 )
508 .out_json()
509 .await?,
510 serde_json::to_value(FinalReceiveOperationState::Claimed)
511 .expect("JSON serialization failed"),
512 );
513
514 Ok(())
515}
516
517#[allow(dead_code)]
518async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
519 if util::FedimintCli::version_or_default().await < *VERSION_0_9_0_ALPHA {
520 return Ok(());
521 }
522
523 if util::FedimintdCmd::version_or_default().await < *VERSION_0_9_0_ALPHA {
524 return Ok(());
525 }
526
527 if util::Gatewayd::version_or_default().await < *VERSION_0_9_0_ALPHA {
528 return Ok(());
529 }
530
531 info!("Testing LNURL pay...");
532
533 let federation = dev_fed.fed().await?;
534
535 let gw_lnd = dev_fed.gw_lnd().await?;
536 let gw_ldk = dev_fed.gw_ldk().await?;
537
538 let gateway_matrix = [
539 (gw_lnd, gw_lnd),
540 (gw_lnd, gw_ldk),
541 (gw_ldk, gw_lnd),
542 (gw_ldk, gw_ldk),
543 ];
544
545 let recurringd = dev_fed.recurringd().await?.api_url().to_string();
546
547 let client_a = federation
548 .new_joined_client("lnv2-lnurl-test-client-a")
549 .await?;
550
551 let client_b = federation
552 .new_joined_client("lnv2-lnurl-test-client-b")
553 .await?;
554
555 federation.pegin_client(10_000, &client_a).await?;
556 federation.pegin_client(10_000, &client_b).await?;
557
558 assert_eq!(client_a.balance().await?, 10_000 * 1000);
559 assert_eq!(client_b.balance().await?, 10_000 * 1000);
560
561 for (gw_send, gw_receive) in gateway_matrix {
562 info!(
563 "Testing lnurl payments: client -> {} -> {} -> client",
564 gw_send.ln.ln_type(),
565 gw_receive.ln.ln_type()
566 );
567
568 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
570 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
571
572 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
573 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
574
575 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
576 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
577
578 let response_a = verify_payment(&verify_url_a).await?;
579 let response_b = verify_payment(&verify_url_b).await?;
580
581 assert!(!response_a.settled);
582 assert!(!response_b.settled);
583
584 assert!(response_a.preimage.is_none());
585 assert!(response_b.preimage.is_none());
586
587 test_send(
588 &client_a,
589 &gw_send.addr,
590 &invoice_b.to_string(),
591 FinalSendOperationState::Success,
592 )
593 .await?;
594
595 test_send(
596 &client_b,
597 &gw_send.addr,
598 &invoice_a.to_string(),
599 FinalSendOperationState::Success,
600 )
601 .await?;
602
603 let response_a = verify_payment(&verify_url_a).await?;
604 let response_b = verify_payment(&verify_url_b).await?;
605
606 assert!(response_a.settled);
607 assert!(response_b.settled);
608
609 let payment_hash = response_a
610 .preimage
611 .expect("Payment A should be settled")
612 .consensus_hash::<sha256::Hash>();
613
614 assert_eq!(payment_hash, *invoice_a.payment_hash());
615
616 let payment_hash = response_b
617 .preimage
618 .expect("Payment B should be settled")
619 .consensus_hash::<sha256::Hash>();
620
621 assert_eq!(payment_hash, *invoice_b.payment_hash());
622
623 assert_eq!(verify_task_a.await??.preimage, response_a.preimage);
624 assert_eq!(verify_task_b.await??.preimage, response_b.preimage);
625 }
626
627 assert_eq!(receive_lnurl(&client_a).await?, 1);
628 assert_eq!(receive_lnurl(&client_a).await?, 2);
629 assert_eq!(receive_lnurl(&client_a).await?, 1);
630
631 assert_eq!(receive_lnurl(&client_b).await?, 3);
632 assert_eq!(receive_lnurl(&client_b).await?, 1);
633
634 Ok(())
635}
636
637async fn receive_lnurl(client: &Client) -> anyhow::Result<u64> {
638 cmd!(
639 client,
640 "module",
641 "lnv2",
642 "lnurl",
643 "receive",
644 "--batch-size",
645 "3"
646 )
647 .out_string()
648 .await
649 .map(|s| s.parse::<u64>().unwrap())
650}
651
652async fn generate_lnurl(
653 client: &Client,
654 recurringd_base_url: &str,
655 gw_ldk_addr: &str,
656) -> anyhow::Result<String> {
657 cmd!(
658 client,
659 "module",
660 "lnv2",
661 "lnurl",
662 "generate",
663 recurringd_base_url,
664 "--gateway",
665 gw_ldk_addr,
666 )
667 .out_json()
668 .await
669 .map(|s| s.as_str().unwrap().to_owned())
670}
671
672async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
673 let response = reqwest::get(verify_url)
674 .await?
675 .json::<VerifyResponse>()
676 .await?;
677
678 Ok(response)
679}
680
681async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
682 let response = reqwest::get(format!("{verify_url}?wait"))
683 .await?
684 .json::<VerifyResponse>()
685 .await?;
686
687 Ok(response)
688}
689
690#[derive(Deserialize, Clone)]
691struct LnUrlPayResponse {
692 callback: String,
693}
694
695#[derive(Deserialize, Clone)]
696struct LnUrlPayInvoiceResponse {
697 pr: Bolt11Invoice,
698 verify: String,
699}
700
701async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
702 let endpoint = LnUrl::from_str(&lnurl)?;
703
704 let response = reqwest::get(endpoint.url)
705 .await?
706 .json::<LnUrlPayResponse>()
707 .await?;
708
709 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
710
711 let response = reqwest::get(callback_url)
712 .await?
713 .json::<LnUrlPayInvoiceResponse>()
714 .await?;
715
716 ensure!(
717 response.pr.amount_milli_satoshis() == Some(amount_msat),
718 "Invoice amount is not set"
719 );
720
721 Ok((response.pr, response.verify))
722}