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_9_0_ALPHA;
9use devimint::{Gatewayd, cmd, util};
10use fedimint_core::core::OperationId;
11use fedimint_core::encoding::Encodable;
12use fedimint_core::task::{self};
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.clone();
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 info!("Testing payment summary...");
333
334 let lnd_payment_summary = gw_lnd.payment_summary().await?;
335
336 assert_eq!(
337 lnd_payment_summary.outgoing.total_success,
338 4 + lnv1_swap + lnv2_swap
339 );
340 assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
341 assert_eq!(lnd_payment_summary.incoming.total_success, 3 + lnv1_swap);
343 assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
344
345 assert!(lnd_payment_summary.outgoing.median_latency.is_some());
346 assert!(lnd_payment_summary.outgoing.average_latency.is_some());
347 assert!(lnd_payment_summary.incoming.median_latency.is_some());
348 assert!(lnd_payment_summary.incoming.average_latency.is_some());
349
350 let ldk_payment_summary = gw_ldk.payment_summary().await?;
351
352 assert_eq!(ldk_payment_summary.outgoing.total_success, 4);
353 assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
354 assert_eq!(ldk_payment_summary.incoming.total_success, 4);
355 assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
356
357 assert!(ldk_payment_summary.outgoing.median_latency.is_some());
358 assert!(ldk_payment_summary.outgoing.average_latency.is_some());
359 assert!(ldk_payment_summary.incoming.median_latency.is_some());
360 assert!(ldk_payment_summary.incoming.average_latency.is_some());
361
362 Ok(())
363}
364
365async fn test_fees(
366 fed_id: String,
367 client: &Client,
368 gw_lnd: &Gatewayd,
369 gw_ldk: &Gatewayd,
370 expected_addition: u64,
371) -> anyhow::Result<()> {
372 let gw_lnd_ecash_prev = gw_lnd.ecash_balance(fed_id.clone()).await?;
373
374 let (invoice, receive_op) = receive(client, &gw_ldk.addr, 1_000_000).await?;
375
376 test_send(
377 client,
378 &gw_lnd.addr,
379 &invoice.to_string(),
380 FinalSendOperationState::Success,
381 )
382 .await?;
383
384 await_receive_claimed(client, receive_op).await?;
385
386 let gw_lnd_ecash_after = gw_lnd.ecash_balance(fed_id.clone()).await?;
387
388 almost_equal(
389 gw_lnd_ecash_prev + expected_addition,
390 gw_lnd_ecash_after,
391 5000,
392 )
393 .unwrap();
394
395 Ok(())
396}
397
398async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
399 cmd!(
400 client,
401 "--our-id",
402 peer.to_string(),
403 "--password",
404 "pass",
405 "module",
406 "lnv2",
407 "gateways",
408 "add",
409 gateway
410 )
411 .out_json()
412 .await?
413 .as_bool()
414 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
415}
416
417async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
418 cmd!(
419 client,
420 "--our-id",
421 peer.to_string(),
422 "--password",
423 "pass",
424 "module",
425 "lnv2",
426 "gateways",
427 "remove",
428 gateway
429 )
430 .out_json()
431 .await?
432 .as_bool()
433 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
434}
435
436async fn receive(
437 client: &Client,
438 gateway: &str,
439 amount: u64,
440) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
441 Ok(serde_json::from_value::<(Bolt11Invoice, OperationId)>(
442 cmd!(
443 client,
444 "module",
445 "lnv2",
446 "receive",
447 amount,
448 "--gateway",
449 gateway
450 )
451 .out_json()
452 .await?,
453 )?)
454}
455
456async fn test_send(
457 client: &Client,
458 gateway: &String,
459 invoice: &String,
460 final_state: FinalSendOperationState,
461) -> anyhow::Result<()> {
462 let send_op = serde_json::from_value::<OperationId>(
463 cmd!(
464 client,
465 "module",
466 "lnv2",
467 "send",
468 invoice,
469 "--gateway",
470 gateway
471 )
472 .out_json()
473 .await?,
474 )?;
475
476 assert_eq!(
477 cmd!(
478 client,
479 "module",
480 "lnv2",
481 "await-send",
482 serde_json::to_string(&send_op)?.substring(1, 65)
483 )
484 .out_json()
485 .await?,
486 serde_json::to_value(final_state).expect("JSON serialization failed"),
487 );
488
489 Ok(())
490}
491
492async fn receive_lnv1(
493 client: &Client,
494 gateway_id: &String,
495 amount_msats: u64,
496) -> anyhow::Result<(Bolt11Invoice, OperationId)> {
497 let invoice_response = cmd!(
498 client,
499 "module",
500 "ln",
501 "invoice",
502 amount_msats,
503 "--gateway-id",
504 gateway_id
505 )
506 .out_json()
507 .await?;
508 let invoice = serde_json::from_value::<Bolt11Invoice>(
509 invoice_response
510 .get("invoice")
511 .expect("Invoice should be present")
512 .clone(),
513 )?;
514 let operation_id = serde_json::from_value::<OperationId>(
515 invoice_response
516 .get("operation_id")
517 .expect("OperationId should be present")
518 .clone(),
519 )?;
520 Ok((invoice, operation_id))
521}
522
523async fn test_send_lnv1(client: &Client, gateway_id: &str, invoice: &str) -> anyhow::Result<()> {
524 let payment_result = cmd!(
525 client,
526 "module",
527 "ln",
528 "pay",
529 invoice,
530 "--gateway-id",
531 gateway_id
532 )
533 .out_json()
534 .await?;
535 assert!(payment_result.get("Success").is_some() || payment_result.get("preimage").is_some());
536 Ok(())
537}
538
539async fn await_receive_claimed(client: &Client, operation_id: OperationId) -> anyhow::Result<()> {
540 assert_eq!(
541 cmd!(
542 client,
543 "module",
544 "lnv2",
545 "await-receive",
546 serde_json::to_string(&operation_id)?.substring(1, 65)
547 )
548 .out_json()
549 .await?,
550 serde_json::to_value(FinalReceiveOperationState::Claimed)
551 .expect("JSON serialization failed"),
552 );
553
554 Ok(())
555}
556
557async fn await_receive_lnv1(client: &Client, operation_id: OperationId) -> anyhow::Result<()> {
558 let lnv1_response = cmd!(client, "await-invoice", operation_id.fmt_full())
559 .out_json()
560 .await?;
561 assert!(lnv1_response.get("total_amount_msat").is_some());
562 Ok(())
563}
564async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
565 if util::FedimintCli::version_or_default().await < *VERSION_0_9_0_ALPHA {
566 return Ok(());
567 }
568
569 if util::FedimintdCmd::version_or_default().await < *VERSION_0_9_0_ALPHA {
570 return Ok(());
571 }
572
573 if util::Gatewayd::version_or_default().await < *VERSION_0_9_0_ALPHA {
574 return Ok(());
575 }
576
577 info!("Testing LNURL pay...");
578
579 let federation = dev_fed.fed().await?;
580
581 let gw_lnd = dev_fed.gw_lnd().await?;
582 let gw_ldk = dev_fed.gw_ldk().await?;
583
584 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
585
586 let recurringd = dev_fed.recurringd().await?.api_url().to_string();
587
588 let client_a = federation
589 .new_joined_client("lnv2-lnurl-test-client-a")
590 .await?;
591
592 let client_b = federation
593 .new_joined_client("lnv2-lnurl-test-client-b")
594 .await?;
595
596 for (gw_send, gw_receive) in gateway_pairs {
597 info!(
598 "Testing lnurl payments: {} -> {} -> client",
599 gw_send.ln.ln_type(),
600 gw_receive.ln.ln_type()
601 );
602
603 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
604 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
605
606 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
607 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
608
609 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
610 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
611
612 let response_a = verify_payment(&verify_url_a).await?;
613 let response_b = verify_payment(&verify_url_b).await?;
614
615 assert!(!response_a.settled);
616 assert!(!response_b.settled);
617
618 assert!(response_a.preimage.is_none());
619 assert!(response_b.preimage.is_none());
620
621 gw_send.pay_invoice(invoice_a.clone()).await?;
622 gw_send.pay_invoice(invoice_b.clone()).await?;
623
624 let response_a = verify_payment(&verify_url_a).await?;
625 let response_b = verify_payment(&verify_url_b).await?;
626
627 assert!(response_a.settled);
628 assert!(response_b.settled);
629
630 let payment_hash = response_a
631 .preimage
632 .expect("Payment A should be settled")
633 .consensus_hash::<sha256::Hash>();
634
635 assert_eq!(payment_hash, *invoice_a.payment_hash());
636
637 let payment_hash = response_b
638 .preimage
639 .expect("Payment B should be settled")
640 .consensus_hash::<sha256::Hash>();
641
642 assert_eq!(payment_hash, *invoice_b.payment_hash());
643
644 assert_eq!(verify_task_a.await??.preimage, response_a.preimage);
645 assert_eq!(verify_task_b.await??.preimage, response_b.preimage);
646 }
647
648 while client_a.balance().await? < 950 * 1000 {
649 info!("Waiting for client A to receive funds via LNURL...");
650
651 cmd!(client_a, "dev", "wait", "1").out_json().await?;
652 }
653
654 info!("Client A successfully received funds via LNURL!");
655
656 while client_b.balance().await? < 950 * 1000 {
657 info!("Waiting for client B to receive funds via LNURL...");
658
659 cmd!(client_b, "dev", "wait", "1").out_json().await?;
660 }
661
662 info!("Client B successfully received funds via LNURL!");
663
664 Ok(())
665}
666
667async fn generate_lnurl(
668 client: &Client,
669 recurringd_base_url: &str,
670 gw_ldk_addr: &str,
671) -> anyhow::Result<String> {
672 cmd!(
673 client,
674 "module",
675 "lnv2",
676 "lnurl",
677 "generate",
678 recurringd_base_url,
679 "--gateway",
680 gw_ldk_addr,
681 )
682 .out_json()
683 .await
684 .map(|s| s.as_str().unwrap().to_owned())
685}
686
687async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
688 let response = reqwest::get(verify_url)
689 .await?
690 .json::<VerifyResponse>()
691 .await?;
692
693 Ok(response)
694}
695
696async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
697 let response = reqwest::get(format!("{verify_url}?wait"))
698 .await?
699 .json::<VerifyResponse>()
700 .await?;
701
702 Ok(response)
703}
704
705#[derive(Deserialize, Clone)]
706struct LnUrlPayResponse {
707 callback: String,
708}
709
710#[derive(Deserialize, Clone)]
711struct LnUrlPayInvoiceResponse {
712 pr: Bolt11Invoice,
713 verify: String,
714}
715
716async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
717 let endpoint = LnUrl::from_str(&lnurl)?;
718
719 let response = reqwest::get(endpoint.url)
720 .await?
721 .json::<LnUrlPayResponse>()
722 .await?;
723
724 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
725
726 let response = reqwest::get(callback_url)
727 .await?
728 .json::<LnUrlPayInvoiceResponse>()
729 .await?;
730
731 ensure!(
732 response.pr.amount_milli_satoshis() == Some(amount_msat),
733 "Invoice amount is not set"
734 );
735
736 Ok((response.pr, response.verify))
737}