1use std::path::PathBuf;
2use std::str::FromStr;
3use std::time::Duration;
4
5use anyhow::{Context as _, bail, ensure};
6use bitcoin::hashes::{Hash, sha256};
7use clap::{Parser, Subcommand};
8use devimint::devfed::{DevFed, DevJitFed};
9use devimint::envs::FM_CLIENT_DIR_ENV;
10use devimint::federation::{Client, Federation};
11use devimint::util::{ProcessManager, almost_equal, poll_simple};
12use devimint::version_constants::{
13 VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA, VERSION_0_12_0_ALPHA, VERSION_0_12_2_ALPHA,
14 VERSION_0_13_0_ALPHA,
15};
16use devimint::{Gatewayd, cmd, util};
17use fedimint_core::core::OperationId;
18use fedimint_core::encoding::Encodable;
19use fedimint_core::secp256k1::rand::rngs::OsRng;
20use fedimint_core::secp256k1::{SECP256K1, SecretKey};
21use fedimint_core::task::{self};
22use fedimint_core::util::{backoff_util, retry, write_overwrite_async};
23use fedimint_lnurl::{LnurlResponse, VerifyResponse, parse_lnurl};
24use fedimint_lnv2_client::FinalSendOperationState;
25use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescriptionRef};
26use serde::Deserialize;
27use serde_json::Value;
28use tokio::try_join;
29use tracing::info;
30
31#[path = "common.rs"]
32mod common;
33
34async fn module_is_present(client: &Client, kind: &str) -> anyhow::Result<bool> {
35 let modules = cmd!(client, "module").out_json().await?;
36
37 let modules = modules["list"].as_array().expect("module list is an array");
38
39 Ok(modules.iter().any(|m| m["kind"].as_str() == Some(kind)))
40}
41
42async fn assert_module_sanity(client: &Client) -> anyhow::Result<()> {
43 if !devimint::util::is_backwards_compatibility_test() {
44 ensure!(
45 !module_is_present(client, "ln").await?,
46 "ln module should not be present"
47 );
48 }
49
50 Ok(())
51}
52
53#[derive(Parser)]
54#[command(name = "lnv2-module-tests")]
55#[command(about = "LNv2 module integration tests", long_about = None)]
56struct Cli {
57 #[command(subcommand)]
58 command: Option<Commands>,
59}
60
61#[derive(Subcommand)]
62enum Commands {
63 GatewayRegistration,
65 Payments,
67 LnurlPay,
69 LnurlRecovery,
71 DuplicatePayment,
73 FederationOutage,
76 DirectHtlc,
78}
79
80#[tokio::main]
81async fn main() -> anyhow::Result<()> {
82 let cli = Cli::parse();
83
84 let num_feds = if matches!(cli.command, None | Some(Commands::DuplicatePayment)) {
87 2
88 } else {
89 1
90 };
91
92 devimint::run_devfed_test()
93 .num_feds(num_feds)
94 .call(|dev_fed, process_mgr| async move {
95 if !devimint::util::supports_lnv2() {
96 info!("lnv2 is disabled, skipping");
97 return Ok(());
98 }
99
100 match &cli.command {
101 Some(Commands::GatewayRegistration) => {
102 test_gateway_registration(&dev_fed).await?;
103 }
104 Some(Commands::Payments) => {
105 test_payments(&dev_fed).await?;
106 }
107 Some(Commands::LnurlPay) => {
108 pegin_gateways(&dev_fed).await?;
109 test_lnurl_pay(&dev_fed).await?;
110 }
111 Some(Commands::LnurlRecovery) => {
112 pegin_gateways(&dev_fed).await?;
113 test_lnurl_recovery(&dev_fed).await?;
114 }
115 Some(Commands::DuplicatePayment) => {
116 pegin_gateways(&dev_fed).await?;
117 test_duplicate_payment(&dev_fed, &process_mgr).await?;
118 }
119 Some(Commands::DirectHtlc) => {
120 test_direct_htlc(&dev_fed).await?;
121 }
122 Some(Commands::FederationOutage) => {
123 pegin_gateways(&dev_fed).await?;
124 let dev_fed = dev_fed.to_dev_fed(&process_mgr).await?;
125 test_federation_outage(dev_fed, &process_mgr).await?;
126 }
127 None => {
128 test_gateway_registration(&dev_fed).await?;
130 test_payments(&dev_fed).await?;
131 test_duplicate_payment(&dev_fed, &process_mgr).await?;
132 test_lnurl_pay(&dev_fed).await?;
133 test_lnurl_recovery(&dev_fed).await?;
134 test_direct_htlc(&dev_fed).await?;
135 pegin_gateways(&dev_fed).await?;
138 let dev_fed = dev_fed.to_dev_fed(&process_mgr).await?;
139 test_federation_outage(dev_fed, &process_mgr).await?;
140 }
141 }
142
143 info!("Testing LNV2 is complete!");
144
145 Ok(())
146 })
147 .await
148}
149
150async fn pegin_gateways(dev_fed: &DevJitFed) -> anyhow::Result<()> {
151 info!("Pegging-in gateways...");
152
153 let federation = dev_fed.fed().await?;
154
155 let gw_lnd = dev_fed.gw_lnd().await?;
156 let gw_ldk = dev_fed.gw_ldk().await?;
157
158 federation
159 .pegin_gateways(1_000_000, vec![gw_lnd, gw_ldk])
160 .await?;
161
162 Ok(())
163}
164
165async fn test_gateway_registration(dev_fed: &DevJitFed) -> anyhow::Result<()> {
166 let client = dev_fed
167 .fed()
168 .await?
169 .new_joined_client("lnv2-test-gateway-registration-client")
170 .await?;
171
172 assert_module_sanity(&client).await?;
173
174 let gw_lnd = dev_fed.gw_lnd().await?;
175 let gw_ldk = dev_fed.gw_ldk_connected().await?;
176
177 let gateways = [gw_lnd.addr.clone(), gw_ldk.addr.clone()];
178
179 info!("Removing the gateways devimint added on startup...");
180
181 let peers = (0..dev_fed.fed().await?.members.len()).collect::<Vec<usize>>();
182 remove_all_gateways(&client, &peers).await?;
183
184 info!("Testing registration of gateways...");
185
186 for gateway in &gateways {
187 for peer in 0..dev_fed.fed().await?.members.len() {
188 assert!(add_gateway(&client, peer, gateway).await?);
189 }
190 }
191
192 assert_eq!(
193 cmd!(client, "module", "lnv2", "gateways", "list")
194 .out_json()
195 .await?
196 .as_array()
197 .expect("JSON Value is not an array")
198 .len(),
199 2
200 );
201
202 assert_eq!(
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 .len(),
209 2
210 );
211
212 info!("Testing selection of gateways...");
213
214 assert!(
215 gateways.contains(
216 &cmd!(client, "module", "lnv2", "gateways", "select")
217 .out_json()
218 .await?
219 .as_str()
220 .expect("JSON Value is not a string")
221 .to_string()
222 )
223 );
224
225 cmd!(client, "module", "lnv2", "gateways", "map")
226 .out_json()
227 .await?;
228
229 for _ in 0..10 {
230 for gateway in &gateways {
231 let invoice = common::receive(&client, gateway, 1_000_000).await?.0;
232
233 assert_eq!(
234 cmd!(
235 client,
236 "module",
237 "lnv2",
238 "gateways",
239 "select",
240 "--invoice",
241 invoice.to_string()
242 )
243 .out_json()
244 .await?
245 .as_str()
246 .expect("JSON Value is not a string"),
247 gateway
248 )
249 }
250 }
251
252 info!("Testing deregistration of gateways...");
253
254 for gateway in &gateways {
255 for peer in 0..dev_fed.fed().await?.members.len() {
256 assert!(remove_gateway(&client, peer, gateway).await?);
257 }
258 }
259
260 assert!(
261 cmd!(client, "module", "lnv2", "gateways", "list")
262 .out_json()
263 .await?
264 .as_array()
265 .expect("JSON Value is not an array")
266 .is_empty(),
267 );
268
269 assert!(
270 cmd!(client, "module", "lnv2", "gateways", "list", "--peer", "0")
271 .out_json()
272 .await?
273 .as_array()
274 .expect("JSON Value is not an array")
275 .is_empty()
276 );
277
278 Ok(())
279}
280
281async fn test_payments(dev_fed: &DevJitFed) -> anyhow::Result<()> {
282 let federation = dev_fed.fed().await?;
283
284 let client = federation
285 .new_joined_client("lnv2-test-payments-client")
286 .await?;
287
288 assert_module_sanity(&client).await?;
289
290 federation.pegin_client(10_000, &client).await?;
291
292 almost_equal(client.balance().await?, 10_000 * 1000, 500_000).unwrap();
293
294 let gw_lnd = dev_fed.gw_lnd().await?;
295 let gw_ldk = dev_fed.gw_ldk().await?;
296 let lnd = dev_fed.lnd().await?;
297
298 let test_hold_invoice = gw_lnd.gatewayd_version >= *VERSION_0_12_0_ALPHA;
307
308 let hold_invoice = if test_hold_invoice {
309 Some(lnd.create_hold_invoice(60000).await?)
310 } else {
311 info!(
312 gatewayd_version = %gw_lnd.gatewayd_version,
313 "Skipping HOLD invoice payment: gateway cancels HOLD invoices it did not create"
314 );
315
316 None
317 };
318
319 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
320
321 let gateway_matrix = [
322 (gw_lnd, gw_lnd),
323 (gw_lnd, gw_ldk),
324 (gw_ldk, gw_lnd),
325 (gw_ldk, gw_ldk),
326 ];
327
328 info!("Testing refund of circular payments...");
329
330 for (gw_send, gw_receive) in gateway_matrix {
331 info!(
332 "Testing refund of payment: client -> {} -> {} -> client",
333 gw_send.ln.ln_type(),
334 gw_receive.ln.ln_type()
335 );
336
337 let invoice = common::receive(&client, &gw_receive.addr, 1_000_000)
338 .await?
339 .0;
340
341 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
342 assert!(matches!(state, FinalSendOperationState::Refunded));
343 }
344
345 pegin_gateways(dev_fed).await?;
346
347 info!("Testing circular payments...");
348
349 for (gw_send, gw_receive) in gateway_matrix {
350 info!(
351 "Testing payment: client -> {} -> {} -> client",
352 gw_send.ln.ln_type(),
353 gw_receive.ln.ln_type()
354 );
355
356 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
357
358 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
359 assert!(matches!(state, FinalSendOperationState::Success(_)));
360
361 common::await_receive_claimed(&client, receive_op).await?;
362 }
363
364 info!("Testing payments from client to gateways...");
365
366 for (gw_send, gw_receive) in gateway_pairs {
367 info!(
368 "Testing payment: client -> {} -> {}",
369 gw_send.ln.ln_type(),
370 gw_receive.ln.ln_type()
371 );
372
373 let invoice = gw_receive.client().create_invoice(1_000_000).await?;
374
375 let state = common::send(&client, &gw_send.addr, &invoice.to_string()).await?;
376 assert!(matches!(state, FinalSendOperationState::Success(_)));
377 }
378
379 info!("Testing payments from gateways to client...");
380
381 for (gw_send, gw_receive) in gateway_pairs {
382 info!(
383 "Testing payment: {} -> {} -> client",
384 gw_send.ln.ln_type(),
385 gw_receive.ln.ln_type()
386 );
387
388 let (invoice, receive_op) = common::receive(&client, &gw_receive.addr, 1_000_000).await?;
389
390 gw_send.client().pay_invoice(invoice).await?;
391
392 common::await_receive_claimed(&client, receive_op).await?;
393 }
394
395 retry(
396 "Waiting for the full balance to become available to the client".to_string(),
397 backoff_util::background_backoff(),
398 || async {
399 ensure!(client.balance().await? >= 9000 * 1000);
400
401 Ok(())
402 },
403 )
404 .await?;
405
406 if let Some((hold_preimage, hold_invoice, hold_payment_hash)) = hold_invoice {
407 info!("Testing Client can pay LND HOLD invoice via LDK Gateway...");
408
409 let (state, _) = try_join!(
410 common::send(&client, &gw_ldk.addr, &hold_invoice),
411 lnd.settle_hold_invoice(hold_preimage, hold_payment_hash),
412 )?;
413 assert!(matches!(state, FinalSendOperationState::Success(_)));
414 }
415
416 info!("Testing LNv2 lightning fees...");
417
418 let fed_id = federation.calculate_federation_id();
419
420 gw_lnd
421 .client()
422 .set_federation_routing_fee(fed_id.clone(), 0, 0)
423 .await?;
424
425 gw_lnd
426 .client()
427 .set_federation_transaction_fee(fed_id.clone(), 0, 0)
428 .await?;
429
430 test_fees(fed_id, &client, gw_lnd, gw_ldk, 1_000_000 - 1_000).await?;
433
434 let online_peers: Vec<usize> = federation.members.keys().copied().collect();
435
436 test_iroh_payment(&client, gw_lnd, gw_ldk, &online_peers).await?;
437
438 info!("Testing payment summary...");
439
440 let lnd_payment_summary = gw_lnd.client().payment_summary().await?;
441
442 assert_eq!(lnd_payment_summary.outgoing.total_success, 5);
443 assert_eq!(lnd_payment_summary.outgoing.total_failure, 2);
444 assert_eq!(lnd_payment_summary.incoming.total_success, 4);
445 assert_eq!(lnd_payment_summary.incoming.total_failure, 0);
446
447 assert!(lnd_payment_summary.outgoing.median_latency.is_some());
448 assert!(lnd_payment_summary.outgoing.average_latency.is_some());
449 assert!(lnd_payment_summary.incoming.median_latency.is_some());
450 assert!(lnd_payment_summary.incoming.average_latency.is_some());
451
452 let ldk_payment_summary = gw_ldk.client().payment_summary().await?;
453
454 assert_eq!(
455 ldk_payment_summary.outgoing.total_success,
456 if test_hold_invoice { 4 } else { 3 }
457 );
458 assert_eq!(ldk_payment_summary.outgoing.total_failure, 2);
459 assert_eq!(ldk_payment_summary.incoming.total_success, 4);
460 assert_eq!(ldk_payment_summary.incoming.total_failure, 0);
461
462 assert!(ldk_payment_summary.outgoing.median_latency.is_some());
463 assert!(ldk_payment_summary.outgoing.average_latency.is_some());
464 assert!(ldk_payment_summary.incoming.median_latency.is_some());
465 assert!(ldk_payment_summary.incoming.average_latency.is_some());
466
467 Ok(())
468}
469
470async fn join_client(name: &str, invite_code: &str) -> anyhow::Result<Client> {
474 let client = Client::create(name).await?;
475 client.join_federation(invite_code.to_string()).await?;
476 Ok(client)
477}
478
479async fn test_duplicate_payment(
487 dev_fed: &DevJitFed,
488 process_mgr: &ProcessManager,
489) -> anyhow::Result<()> {
490 info!(
491 "Testing three clients across two federations paying the same invoice settle exactly once..."
492 );
493
494 let gw_send = dev_fed.gw_ldk().await?;
497 let gw_receive = dev_fed.gw_lnd().await?;
498
499 if gw_send.gatewayd_version < *VERSION_0_12_0_ALPHA {
502 info!(
503 gatewayd_version = %gw_send.gatewayd_version,
504 "Skipping: gateway predates cross-federation outgoing payment dedup"
505 );
506 return Ok(());
507 }
508
509 let first_federation = dev_fed.fed().await?;
510
511 let first_invite = first_federation.invite_code()?;
516
517 let second_federation = Federation::new(
521 process_mgr,
522 dev_fed.bitcoind().await?.clone(),
523 false,
524 false,
525 false,
526 1,
527 "lnv2-duplicate-payment".to_string(),
528 )
529 .await?;
530 let second_invite = second_federation.invite_code()?;
531
532 assert_ne!(
533 first_federation.calculate_federation_id(),
534 second_federation.calculate_federation_id(),
535 "the second federation must be distinct from the first"
536 );
537
538 let client_dir: PathBuf = std::env::var(FM_CLIENT_DIR_ENV)?.parse()?;
544 write_overwrite_async(client_dir.join("invite-code"), &first_invite).await?;
545
546 gw_send.client().connect_fed(second_invite.clone()).await?;
547 second_federation
548 .pegin_gateways(1_000_000, vec![gw_send])
549 .await?;
550
551 let client_a = join_client("lnv2-duplicate-payment-a", &first_invite).await?;
552 let client_b = join_client("lnv2-duplicate-payment-b", &first_invite).await?;
553 let client_c = join_client("lnv2-duplicate-payment-c", &second_invite).await?;
554
555 first_federation.pegin_client(10_000, &client_a).await?;
556 first_federation.pegin_client(10_000, &client_b).await?;
557 second_federation.pegin_client(10_000, &client_c).await?;
558
559 let second_federation_id = second_federation.calculate_federation_id();
564 let second_balance_before_control = gw_send
565 .client()
566 .ecash_balance(second_federation_id.clone())
567 .await?;
568 let control_invoice = gw_receive
569 .client()
570 .create_invoice(1_000_000)
571 .await?
572 .to_string();
573 let control = common::send(&client_c, &gw_send.addr, &control_invoice).await?;
574 assert!(
575 matches!(control, FinalSendOperationState::Success(_)),
576 "second-federation control payment must succeed, got {control:?}"
577 );
578
579 poll_simple(
583 "Waiting for the gateway to claim the control payment",
584 || async {
585 let balance = gw_send
586 .client()
587 .ecash_balance(second_federation_id.clone())
588 .await?;
589 ensure!(
590 balance > second_balance_before_control,
591 "gateway has not claimed the control payment yet"
592 );
593 Ok(())
594 },
595 )
596 .await?;
597
598 let invoice = gw_receive
599 .client()
600 .create_invoice(1_000_000)
601 .await?
602 .to_string();
603
604 if gw_send.gatewayd_version < *VERSION_0_12_2_ALPHA {
609 return assert_duplicates_refunded(&client_a, &client_b, &client_c, gw_send, &invoice)
610 .await;
611 }
612
613 assert_duplicates_refused(
614 [&client_a, &client_b, &client_c],
615 gw_send,
616 [
617 first_federation.calculate_federation_id(),
618 second_federation_id,
619 ],
620 &invoice,
621 )
622 .await
623}
624
625const REFUSED_SEND_WATCH: Duration = Duration::from_secs(30);
627
628async fn assert_duplicates_refunded(
631 client_a: &Client,
632 client_b: &Client,
633 client_c: &Client,
634 gw_send: &Gatewayd,
635 invoice: &str,
636) -> anyhow::Result<()> {
637 let (state_a, state_b) = try_join!(
640 common::send(client_a, &gw_send.addr, invoice),
641 common::send(client_b, &gw_send.addr, invoice),
642 )?;
643
644 let state_c = common::send(client_c, &gw_send.addr, invoice).await?;
648
649 let states = [state_a, state_b, state_c];
650
651 info!("shared-invoice send states: {states:?}");
652
653 let successes = states
654 .iter()
655 .filter(|state| matches!(state, FinalSendOperationState::Success(_)))
656 .count();
657
658 let refunds = states
659 .iter()
660 .filter(|state| matches!(state, FinalSendOperationState::Refunded))
661 .count();
662
663 assert_eq!(
664 (successes, refunds),
665 (1, 2),
666 "exactly one payment must settle and the other two be refunded, got {states:?}"
667 );
668
669 Ok(())
670}
671
672async fn assert_duplicates_refused(
677 [client_a, client_b, client_c]: [&Client; 3],
678 gw_send: &Gatewayd,
679 [first_federation_id, second_federation_id]: [String; 2],
680 invoice: &str,
681) -> anyhow::Result<()> {
682 let first_balance_before = gw_send
683 .client()
684 .ecash_balance(first_federation_id.clone())
685 .await?;
686 let second_balance_before = gw_send
687 .client()
688 .ecash_balance(second_federation_id.clone())
689 .await?;
690
691 let (op_a, op_b) = try_join!(
694 common::start_send(client_a, &gw_send.addr, invoice),
695 common::start_send(client_b, &gw_send.addr, invoice),
696 )?;
697
698 let (state_a, state_b) = tokio::join!(
699 await_send_within(client_a, op_a, REFUSED_SEND_WATCH),
700 await_send_within(client_b, op_b, REFUSED_SEND_WATCH),
701 );
702
703 let op_c = common::start_send(client_c, &gw_send.addr, invoice).await?;
706 let state_c = await_send_within(client_c, op_c, REFUSED_SEND_WATCH).await;
707
708 let states = [state_a?, state_b?, state_c?];
709
710 info!("shared-invoice send states (None = unresolved): {states:?}");
711
712 let successes = states
713 .iter()
714 .filter(|state| matches!(state, Some(FinalSendOperationState::Success(_))))
715 .count();
716
717 let unresolved = states.iter().filter(|state| state.is_none()).count();
718
719 assert_eq!(
720 (successes, unresolved),
721 (1, 2),
722 "exactly one payment must settle and the other two stay unresolved, got {states:?}"
723 );
724
725 let invoice_msats = Bolt11Invoice::from_str(invoice)?
728 .amount_milli_satoshis()
729 .expect("the invoice has an amount");
730
731 let first_gain = gw_send
732 .client()
733 .ecash_balance(first_federation_id)
734 .await?
735 .checked_sub(first_balance_before)
736 .expect("the gateway's balance in the first federation must not shrink");
737
738 assert!(
739 (invoice_msats..2 * invoice_msats).contains(&first_gain),
740 "the gateway must be reimbursed for exactly one contract, gained {first_gain} msat"
741 );
742
743 assert_eq!(
744 gw_send.client().ecash_balance(second_federation_id).await?,
745 second_balance_before,
746 "the gateway must not claim the second federation's contract"
747 );
748
749 Ok(())
750}
751
752async fn await_send_within(
755 client: &Client,
756 operation_id: OperationId,
757 watch: Duration,
758) -> anyhow::Result<Option<FinalSendOperationState>> {
759 match tokio::time::timeout(watch, common::await_send(client, operation_id)).await {
760 Ok(state) => state.map(Some),
761 Err(_) => Ok(None),
762 }
763}
764
765async fn test_federation_outage(
769 dev_fed: DevFed,
770 process_mgr: &ProcessManager,
771) -> anyhow::Result<()> {
772 info!("Testing that the gateway refuses to fund a receive during a federation outage...");
773
774 let DevFed {
775 mut fed,
776 gw_lnd,
777 gw_ldk,
778 ..
779 } = dev_fed;
780
781 if gw_lnd.gatewayd_version < *VERSION_0_12_2_ALPHA {
782 info!(
783 gatewayd_version = %gw_lnd.gatewayd_version,
784 "Skipping: gateway predates the federation liveness probe"
785 );
786 return Ok(());
787 }
788
789 let client = fed
790 .new_joined_client("lnv2-federation-outage-client")
791 .await?;
792 fed.await_all_peers().await?;
793
794 let federation_id = fed.calculate_federation_id();
795 let gateway = gw_lnd.client().address();
796
797 let (invoice, _) = common::receive(&client, &gateway, 100_000).await?;
800 let balance_before = gw_lnd.client().ecash_balance(federation_id.clone()).await?;
801
802 let fed_size = process_mgr.globals.FM_FED_SIZE;
805 let max_evil = (fed_size - 1) / 3;
806 let stopped: Vec<usize> = (fed_size - max_evil - 1..fed_size).collect();
807 for peer in &stopped {
808 fed.terminate_server(*peer).await?;
809 }
810
811 info!(
812 ?stopped,
813 "Paying invoice while the federation cannot reach consensus"
814 );
815 let payment = fedimint_core::runtime::spawn("lnv2-outage-payment", {
819 let gw_ldk = gw_ldk.client();
820 async move { gw_ldk.pay_invoice(invoice).await }
821 });
822
823 poll_simple("Waiting for the gateway to act on the HTLC", || async {
827 if payment.is_finished() {
828 return Ok(());
829 }
830 let balance = gw_lnd.client().ecash_balance(federation_id.clone()).await?;
831 if balance < balance_before {
832 return Ok(());
833 }
834 bail!("gateway has not acted on the HTLC yet")
835 })
836 .await?;
837
838 for peer in &stopped {
839 fed.start_server(process_mgr, *peer).await?;
840 }
841 fed.await_all_peers().await?;
842
843 let outcome = payment.await?;
847 info!(?outcome, "Payment outcome once the guardians are back");
848 ensure!(
849 outcome.is_err(),
850 "payment must fail: the gateway funded an incoming contract during the outage"
851 );
852 ensure!(
853 gw_lnd.client().ecash_balance(federation_id.clone()).await? == balance_before,
854 "gateway must not fund an incoming contract while the federation cannot reach consensus"
855 );
856
857 info!("Paying a fresh invoice after the guardians are back");
861 poll_simple("Waiting for receives to succeed again", || async {
862 let (invoice, _) = common::receive(&client, &gateway, 100_000).await?;
863 gw_ldk.client().pay_invoice(invoice).await
864 })
865 .await?;
866 ensure!(
867 gw_lnd.client().ecash_balance(federation_id).await? < balance_before,
868 "gateway must fund the incoming contract once the federation is back"
869 );
870
871 info!("Federation outage test complete");
872 Ok(())
873}
874
875async fn test_fees(
876 fed_id: String,
877 client: &Client,
878 gw_lnd: &Gatewayd,
879 gw_ldk: &Gatewayd,
880 expected_addition: u64,
881) -> anyhow::Result<()> {
882 let gw_lnd_ecash_prev = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
883
884 let (invoice, receive_op) = common::receive(client, &gw_ldk.addr, 1_000_000).await?;
885
886 let state = common::send(client, &gw_lnd.addr, &invoice.to_string()).await?;
887 assert!(matches!(state, FinalSendOperationState::Success(_)));
888
889 common::await_receive_claimed(client, receive_op).await?;
890
891 while almost_equal(
895 gw_lnd_ecash_prev + expected_addition,
896 gw_lnd.client().ecash_balance(fed_id.clone()).await?,
897 5000,
898 )
899 .is_err()
900 {
901 info!("Waiting for the sending gateway's outgoing claim to settle...");
902 cmd!(client, "dev", "wait", "1").out_json().await?;
903 }
904
905 Ok(())
906}
907
908async fn add_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
909 cmd!(
910 client,
911 "--our-id",
912 peer.to_string(),
913 "--password",
914 "pass",
915 "module",
916 "lnv2",
917 "gateways",
918 "add",
919 gateway
920 )
921 .out_json()
922 .await?
923 .as_bool()
924 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
925}
926
927async fn remove_gateway(client: &Client, peer: usize, gateway: &String) -> anyhow::Result<bool> {
928 cmd!(
929 client,
930 "--our-id",
931 peer.to_string(),
932 "--password",
933 "pass",
934 "module",
935 "lnv2",
936 "gateways",
937 "remove",
938 gateway
939 )
940 .out_json()
941 .await?
942 .as_bool()
943 .ok_or(anyhow::anyhow!("JSON Value is not a boolean"))
944}
945
946async fn remove_all_gateways(client: &Client, peers: &[usize]) -> anyhow::Result<()> {
949 let gateways = cmd!(client, "module", "lnv2", "gateways", "list")
950 .out_json()
951 .await?
952 .as_array()
953 .expect("JSON Value is not an array")
954 .iter()
955 .map(|gateway| {
956 gateway
957 .as_str()
958 .expect("JSON Value is not a string")
959 .to_string()
960 })
961 .collect::<Vec<String>>();
962
963 for gateway in &gateways {
964 for &peer in peers {
965 assert!(remove_gateway(client, peer, gateway).await?);
966 }
967 }
968
969 Ok(())
970}
971
972const LNURL_BALANCE_WAIT_ATTEMPTS: u64 = 120;
975
976async fn wait_for_lnurl_balance(
977 client: &Client,
978 client_name: &str,
979 expected_msats: u64,
980) -> anyhow::Result<()> {
981 let mut last_balance_msats = client.balance().await?;
982
983 for attempt in 1..=LNURL_BALANCE_WAIT_ATTEMPTS {
984 if last_balance_msats < expected_msats {
985 info!(
986 client_name,
987 balance_msats = last_balance_msats,
988 expected_msats,
989 attempt,
990 "Waiting for {client_name} to receive funds via LNURL"
991 );
992 cmd!(client, "dev", "wait", "1").out_json().await?;
993 last_balance_msats = client.balance().await?;
994 } else {
995 info!(
996 client_name,
997 balance_msats = last_balance_msats,
998 expected_msats,
999 attempt,
1000 "{client_name} successfully received funds via LNURL"
1001 );
1002 return Ok(());
1003 }
1004 }
1005
1006 if last_balance_msats < expected_msats {
1007 anyhow::bail!(
1008 "timed out waiting for {client_name} to receive funds via LNURL after {} attempts; last balance: {last_balance_msats} msats; expected balance: {expected_msats} msats",
1009 LNURL_BALANCE_WAIT_ATTEMPTS
1010 );
1011 }
1012
1013 info!(
1014 client_name,
1015 balance_msats = last_balance_msats,
1016 expected_msats,
1017 attempt = LNURL_BALANCE_WAIT_ATTEMPTS,
1018 "{client_name} successfully received funds via LNURL"
1019 );
1020
1021 Ok(())
1022}
1023
1024async fn test_lnurl_pay(dev_fed: &DevJitFed) -> anyhow::Result<()> {
1025 if util::FedimintCli::version_or_default().await < *VERSION_0_11_0_ALPHA {
1026 return Ok(());
1027 }
1028
1029 if util::FedimintdCmd::version_or_default().await < *VERSION_0_11_0_ALPHA {
1030 return Ok(());
1031 }
1032
1033 if util::Gatewayd::version_or_default().await < *VERSION_0_11_0_ALPHA {
1034 return Ok(());
1035 }
1036
1037 let federation = dev_fed.fed().await?;
1038
1039 let gw_lnd = dev_fed.gw_lnd().await?;
1040 let gw_ldk = dev_fed.gw_ldk().await?;
1041
1042 let gateway_pairs = [(gw_lnd, gw_ldk), (gw_ldk, gw_lnd)];
1043
1044 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
1045
1046 let client_a = federation
1047 .new_joined_client("lnv2-lnurl-test-client-a")
1048 .await?;
1049
1050 assert_module_sanity(&client_a).await?;
1051
1052 let client_b = federation
1053 .new_joined_client("lnv2-lnurl-test-client-b")
1054 .await?;
1055
1056 assert_module_sanity(&client_b).await?;
1057
1058 for (gw_send, gw_receive) in gateway_pairs {
1059 info!(
1060 "Testing lnurl payments: {} -> {} -> client",
1061 gw_send.ln.ln_type(),
1062 gw_receive.ln.ln_type()
1063 );
1064
1065 let lnurl_a = generate_lnurl(&client_a, &recurringd, &gw_receive.addr).await?;
1066 let lnurl_b = generate_lnurl(&client_b, &recurringd, &gw_receive.addr).await?;
1067
1068 let (invoice_a, verify_url_a) = fetch_invoice(lnurl_a.clone(), 500_000).await?;
1069 let (invoice_b, verify_url_b) = fetch_invoice(lnurl_b.clone(), 500_000).await?;
1070
1071 let verify_task_a = task::spawn("verify_task_a", verify_payment_wait(verify_url_a.clone()));
1072 let verify_task_b = task::spawn("verify_task_b", verify_payment_wait(verify_url_b.clone()));
1073
1074 let response_a = verify_payment(&verify_url_a).await?;
1075 let response_b = verify_payment(&verify_url_b).await?;
1076
1077 assert!(!response_a.settled);
1078 assert!(!response_b.settled);
1079
1080 assert!(response_a.preimage.is_none());
1081 assert!(response_b.preimage.is_none());
1082
1083 gw_send.client().pay_invoice(invoice_a.clone()).await?;
1084 gw_send.client().pay_invoice(invoice_b.clone()).await?;
1085
1086 let response_a = verify_payment(&verify_url_a).await?;
1087 let response_b = verify_payment(&verify_url_b).await?;
1088
1089 assert!(response_a.settled);
1090 assert!(response_b.settled);
1091
1092 verify_preimage(&response_a, &invoice_a);
1093 verify_preimage(&response_b, &invoice_b);
1094
1095 assert_eq!(verify_task_a.await??, response_a);
1096 assert_eq!(verify_task_b.await??, response_b);
1097 }
1098
1099 wait_for_lnurl_balance(&client_a, "client A", 950 * 1000).await?;
1100 wait_for_lnurl_balance(&client_b, "client B", 950 * 1000).await?;
1101
1102 Ok(())
1103}
1104
1105async fn test_lnurl_recovery(dev_fed: &DevJitFed) -> anyhow::Result<()> {
1113 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA {
1117 return Ok(());
1118 }
1119
1120 let federation = dev_fed.fed().await?;
1121 let gw_lnd = dev_fed.gw_lnd().await?;
1122 let gw_ldk = dev_fed.gw_ldk().await?;
1123 let recurringd = dev_fed.recurringdv2().await?.api_url().to_string();
1124
1125 const LNURL_AMOUNT_MSAT: u64 = 500_000;
1126 const LNURL_BALANCE_TOLERANCE_MSAT: u64 = 100_000;
1127
1128 info!("Phase 1: Creating client and receiving via LNURL before recovery");
1131
1132 let client = federation
1133 .new_joined_client("lnv2-lnurl-recovery-original")
1134 .await?;
1135
1136 let lnurl = generate_lnurl(&client, &recurringd, &gw_ldk.addr).await?;
1137
1138 for i in 0..3 {
1139 info!("Paying LNURL invoice {}/3", i + 1);
1140 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
1141 gw_lnd.client().pay_invoice(invoice).await?;
1142 }
1143
1144 while almost_equal(
1145 client.balance().await?,
1146 3 * LNURL_AMOUNT_MSAT,
1147 LNURL_BALANCE_TOLERANCE_MSAT,
1148 )
1149 .is_err()
1150 {
1151 info!("Waiting for pre-recovery LNURL payments to settle...");
1152 cmd!(client, "dev", "wait", "1").out_json().await?;
1153 }
1154
1155 let pre_recovery_balance = client.balance().await?;
1156 info!("Pre-recovery balance: {pre_recovery_balance} msats");
1157
1158 let mnemonic = cmd!(client, "print-secret").out_json().await?["secret"]
1159 .as_str()
1160 .expect("secret is a string")
1161 .to_owned();
1162
1163 info!("Phase 2: Recovering client from seed");
1166
1167 let restored = Client::create("lnv2-lnurl-recovery-restored").await?;
1168 cmd!(
1169 restored,
1170 "restore",
1171 "--invite-code",
1172 federation.invite_code()?,
1173 "--mnemonic",
1174 &mnemonic
1175 )
1176 .run()
1177 .await?;
1178
1179 while restored.balance().await? < pre_recovery_balance {
1180 info!("Waiting for recovery to complete...");
1181 cmd!(restored, "dev", "wait", "1").out_json().await?;
1182 }
1183
1184 let post_recovery_balance = restored.balance().await?;
1185 info!("Post-recovery balance: {post_recovery_balance} msats");
1186
1187 info!("Phase 3: Paying to the original LNURL; restored client must claim");
1190
1191 for i in 0..2 {
1194 info!("Paying pre-recovery LNURL invoice {}/2", i + 1);
1195 let (invoice, _verify_url) = fetch_invoice(lnurl.clone(), LNURL_AMOUNT_MSAT).await?;
1196 gw_lnd.client().pay_invoice(invoice).await?;
1197 }
1198
1199 while almost_equal(
1200 restored.balance().await?,
1201 post_recovery_balance + 2 * LNURL_AMOUNT_MSAT,
1202 LNURL_BALANCE_TOLERANCE_MSAT,
1203 )
1204 .is_err()
1205 {
1206 info!("Waiting for post-recovery LNURL payments to settle...");
1207 cmd!(restored, "dev", "wait", "1").out_json().await?;
1208 }
1209
1210 let final_balance = restored.balance().await?;
1211 info!("Final balance: {final_balance} msats");
1212
1213 let operations = cmd!(restored, "list-operations", "--limit", "100")
1214 .out_json()
1215 .await?;
1216 let lnv2_ops: Vec<_> = operations["operations"]
1217 .as_array()
1218 .expect("operations is an array")
1219 .iter()
1220 .filter(|op| op["operation_kind"].as_str() == Some("lnv2"))
1221 .collect();
1222 assert!(
1223 lnv2_ops.len() >= 2,
1224 "Expected at least 2 LNv2 operations after post-recovery receives, found {}",
1225 lnv2_ops.len()
1226 );
1227
1228 info!(
1229 "LNURL recovery test passed: {} new operations, balance {final_balance} msats",
1230 lnv2_ops.len()
1231 );
1232
1233 Ok(())
1234}
1235
1236async fn generate_lnurl(
1237 client: &Client,
1238 recurringd_base_url: &str,
1239 gw_ldk_addr: &str,
1240) -> anyhow::Result<String> {
1241 cmd!(
1242 client,
1243 "module",
1244 "lnv2",
1245 "lnurl",
1246 "generate",
1247 recurringd_base_url,
1248 "--gateway",
1249 gw_ldk_addr,
1250 )
1251 .out_json()
1252 .await
1253 .map(|s| s.as_str().unwrap().to_owned())
1254}
1255
1256fn verify_preimage(response: &VerifyResponse, invoice: &Bolt11Invoice) {
1257 let preimage = response.preimage.expect("Payment should be settled");
1258
1259 let payment_hash = preimage.consensus_hash::<sha256::Hash>();
1260
1261 assert_eq!(payment_hash, *invoice.payment_hash());
1262}
1263
1264async fn verify_payment(verify_url: &str) -> anyhow::Result<VerifyResponse> {
1265 reqwest::get(verify_url)
1266 .await?
1267 .json::<LnurlResponse<VerifyResponse>>()
1268 .await?
1269 .into_result()
1270 .map_err(anyhow::Error::msg)
1271}
1272
1273async fn verify_payment_wait(verify_url: String) -> anyhow::Result<VerifyResponse> {
1274 reqwest::get(format!("{verify_url}?wait"))
1275 .await?
1276 .json::<LnurlResponse<VerifyResponse>>()
1277 .await?
1278 .into_result()
1279 .map_err(anyhow::Error::msg)
1280}
1281
1282#[derive(Deserialize, Clone)]
1283struct LnUrlPayResponse {
1284 callback: String,
1285 metadata: String,
1286}
1287
1288#[derive(Deserialize, Clone)]
1289struct LnUrlPayInvoiceResponse {
1290 pr: Bolt11Invoice,
1291 routes: Vec<serde_json::Value>,
1293 verify: String,
1294}
1295
1296async fn fetch_invoice(lnurl: String, amount_msat: u64) -> anyhow::Result<(Bolt11Invoice, String)> {
1297 let url = parse_lnurl(&lnurl).ok_or_else(|| anyhow::anyhow!("Invalid LNURL"))?;
1298
1299 let response = reqwest::get(url).await?.json::<LnUrlPayResponse>().await?;
1300
1301 let callback_url = format!("{}?amount={}", response.callback, amount_msat);
1302
1303 let invoice_response = reqwest::get(callback_url)
1304 .await?
1305 .json::<LnUrlPayInvoiceResponse>()
1306 .await?;
1307
1308 ensure!(
1309 invoice_response.pr.amount_milli_satoshis() == Some(amount_msat),
1310 "Invoice amount is not set"
1311 );
1312
1313 ensure!(
1314 invoice_response.routes.is_empty(),
1315 "LUD-06 requires routes to be an empty array"
1316 );
1317
1318 let metadata_hash = sha256::Hash::hash(response.metadata.as_bytes());
1319
1320 ensure!(
1321 matches!(
1322 invoice_response.pr.description(),
1323 Bolt11InvoiceDescriptionRef::Hash(hash) if hash.0 == metadata_hash
1324 ),
1325 "Invoice does not commit to the LNURL metadata via its description hash (LUD-06)"
1326 );
1327
1328 Ok((invoice_response.pr, invoice_response.verify))
1329}
1330
1331async fn test_iroh_payment(
1332 client: &Client,
1333 gw_lnd: &Gatewayd,
1334 gw_ldk: &Gatewayd,
1335 online_peers: &[usize],
1336) -> anyhow::Result<()> {
1337 info!("Testing iroh payment...");
1338 remove_all_gateways(client, online_peers).await?;
1341 for &peer in online_peers {
1342 add_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1343 }
1344
1345 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1348 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1349 {
1350 for &peer in online_peers {
1351 add_gateway(client, peer, &gw_lnd.addr).await?;
1352 }
1353 }
1354
1355 let invoice = gw_ldk.client().create_invoice(5_000_000).await?;
1356
1357 let send_op = serde_json::from_value::<OperationId>(
1358 cmd!(client, "module", "lnv2", "send", invoice,)
1359 .out_json()
1360 .await?,
1361 )?;
1362
1363 let send_state = common::await_send(client, send_op).await?;
1364 assert!(
1365 matches!(send_state, FinalSendOperationState::Success(_)),
1366 "unexpected send state: {send_state:?}"
1367 );
1368
1369 let (invoice, receive_op) = serde_json::from_value::<(Bolt11Invoice, OperationId)>(
1370 cmd!(client, "module", "lnv2", "receive", "5000000",)
1371 .out_json()
1372 .await?,
1373 )?;
1374
1375 gw_ldk.client().pay_invoice(invoice).await?;
1376 common::await_receive_claimed(client, receive_op).await?;
1377
1378 if util::FedimintCli::version_or_default().await < *VERSION_0_10_0_ALPHA
1379 || gw_lnd.gatewayd_version < *VERSION_0_10_0_ALPHA
1380 {
1381 for &peer in online_peers {
1382 remove_gateway(client, peer, &gw_lnd.addr).await?;
1383 }
1384 }
1385
1386 for &peer in online_peers {
1387 remove_gateway(client, peer, &format!("iroh://{}", gw_lnd.node_id)).await?;
1388 }
1389
1390 Ok(())
1391}
1392
1393async fn test_direct_htlc(dev_fed: &DevJitFed) -> anyhow::Result<()> {
1398 if util::FedimintCli::version_or_default().await < *VERSION_0_13_0_ALPHA {
1399 info!("fedimint-cli does not support direct HTLCs, skipping");
1400 return Ok(());
1401 }
1402
1403 let federation = dev_fed.fed().await?;
1404
1405 let funder = federation
1406 .new_joined_client("lnv2-test-direct-htlc-funder")
1407 .await?;
1408
1409 let claimer = federation
1410 .new_joined_client("lnv2-test-direct-htlc-claimer")
1411 .await?;
1412
1413 assert_module_sanity(&funder).await?;
1414
1415 federation.pegin_client(10_000, &funder).await?;
1416
1417 let secrets_dir = PathBuf::from(std::env::var(FM_CLIENT_DIR_ENV)?).join("direct-htlc");
1418
1419 tokio::fs::create_dir_all(&secrets_dir).await?;
1420
1421 let claim_sk_file = secrets_dir.join("claim-sk");
1422 let preimage_file = secrets_dir.join("preimage");
1423
1424 info!("Generating the claim keypair...");
1425
1426 let claim_keypair = cmd!(
1427 claimer,
1428 "module",
1429 "lnv2",
1430 "htlc",
1431 "new-claim-keypair",
1432 "--secret-key-file",
1433 claim_sk_file.display()
1434 )
1435 .out_json()
1436 .await?;
1437
1438 ensure!(
1439 claim_keypair.get("secret_key").is_none(),
1440 "The claim secret key must not be printed"
1441 );
1442
1443 let claim_pk = json_string(&claim_keypair["public_key"])?;
1444
1445 {
1446 use std::os::unix::fs::PermissionsExt as _;
1447
1448 let mode = tokio::fs::metadata(&claim_sk_file)
1449 .await?
1450 .permissions()
1451 .mode();
1452
1453 ensure!(mode & 0o777 == 0o600, "Secret key file has mode {mode:o}");
1454 }
1455
1456 ensure!(
1457 cmd!(
1458 claimer,
1459 "module",
1460 "lnv2",
1461 "htlc",
1462 "new-claim-keypair",
1463 "--secret-key-file",
1464 claim_sk_file.display()
1465 )
1466 .run()
1467 .await
1468 .is_err(),
1469 "An existing secret key file must not be overwritten"
1470 );
1471
1472 info!("Testing claim of a point-locked HTLC...");
1473
1474 let preimage = SecretKey::new(&mut OsRng);
1476 let preimage_hex = preimage.display_secret().to_string();
1477 let payment_point = preimage.public_key(SECP256K1).to_string();
1478
1479 write_overwrite_async(&preimage_file, &preimage_hex).await?;
1480
1481 let (funding_txid, contract) =
1482 create_htlc(&funder, &claim_pk, 100, "--payment-point", &payment_point).await?;
1483
1484 let remaining_blocks = cmd!(
1485 claimer,
1486 "module",
1487 "lnv2",
1488 "htlc",
1489 "await-funded",
1490 funding_txid,
1491 contract
1492 )
1493 .out_json()
1494 .await?;
1495
1496 ensure!(
1497 remaining_blocks.as_u64().is_some_and(|blocks| blocks > 0),
1498 "Expected the remaining blocks until expiration, got {remaining_blocks}"
1499 );
1500
1501 let mut claim = cmd!(
1502 claimer,
1503 "module",
1504 "lnv2",
1505 "htlc",
1506 "claim",
1507 funding_txid,
1508 contract,
1509 "--claim-sk-file",
1510 claim_sk_file.display(),
1511 "--preimage-file",
1512 "-"
1513 );
1514
1515 claim.cmd.stdin(std::fs::File::open(&preimage_file)?);
1516
1517 let claim_operation = claim.out_json().await?;
1518
1519 await_htlc_settled(&claimer, &claim_operation).await?;
1520
1521 ensure!(claimer.balance().await? > 0);
1522
1523 assert_eq!(
1525 cmd!(
1526 funder,
1527 "module",
1528 "lnv2",
1529 "htlc",
1530 "await-resolution",
1531 funding_txid,
1532 contract
1533 )
1534 .out_json()
1535 .await?,
1536 preimage_hex
1537 );
1538
1539 info!("Testing cooperative cancellation of a hash-locked HTLC...");
1540
1541 let payment_hash = [42_u8; 32].consensus_hash::<sha256::Hash>().to_string();
1542
1543 let (funding_txid, contract) =
1544 create_htlc(&funder, &claim_pk, 100, "--payment-hash", &payment_hash).await?;
1545
1546 let balance_before_cancel = funder.balance().await?;
1547
1548 let mut forfeit = cmd!(
1549 claimer,
1550 "module",
1551 "lnv2",
1552 "htlc",
1553 "forfeit",
1554 contract,
1555 "--claim-sk-file",
1556 "-"
1557 );
1558
1559 forfeit.cmd.stdin(std::fs::File::open(&claim_sk_file)?);
1560
1561 let forfeit_signature = json_string(&forfeit.out_json().await?)?;
1562
1563 let cancel_operation = cmd!(
1564 funder,
1565 "module",
1566 "lnv2",
1567 "htlc",
1568 "cancel",
1569 funding_txid,
1570 contract,
1571 forfeit_signature
1572 )
1573 .out_json()
1574 .await?;
1575
1576 await_htlc_settled(&funder, &cancel_operation).await?;
1577
1578 ensure!(funder.balance().await? > balance_before_cancel);
1579
1580 info!("Testing refund of an expired HTLC...");
1581
1582 let (funding_txid, contract) =
1583 create_htlc(&funder, &claim_pk, 5, "--payment-hash", &payment_hash).await?;
1584
1585 let balance_before_refund = funder.balance().await?;
1586
1587 dev_fed.bitcoind().await?.mine_blocks(10).await?;
1588
1589 assert_eq!(
1591 cmd!(
1592 funder,
1593 "module",
1594 "lnv2",
1595 "htlc",
1596 "await-resolution",
1597 funding_txid,
1598 contract
1599 )
1600 .out_json()
1601 .await?,
1602 Value::Null
1603 );
1604
1605 let refund_operation = retry(
1607 "Refunding the expired HTLC",
1608 backoff_util::aggressive_backoff_long(),
1609 || async {
1610 cmd!(
1611 funder,
1612 "module",
1613 "lnv2",
1614 "htlc",
1615 "refund",
1616 funding_txid,
1617 contract
1618 )
1619 .out_json()
1620 .await
1621 },
1622 )
1623 .await?;
1624
1625 await_htlc_settled(&funder, &refund_operation).await?;
1626
1627 ensure!(funder.balance().await? > balance_before_refund);
1628
1629 Ok(())
1630}
1631
1632async fn create_htlc(
1636 funder: &Client,
1637 claim_pk: &str,
1638 expiration_delta: u64,
1639 lock_flag: &str,
1640 lock_value: &str,
1641) -> anyhow::Result<(String, String)> {
1642 let created = cmd!(
1643 funder,
1644 "module",
1645 "lnv2",
1646 "htlc",
1647 "create",
1648 "1000000",
1649 claim_pk,
1650 expiration_delta,
1651 lock_flag,
1652 lock_value
1653 )
1654 .out_json()
1655 .await?;
1656
1657 assert_eq!(created["outpoint"]["out_idx"], 0);
1659
1660 await_htlc_settled(funder, &created["operation_id"]).await?;
1661
1662 Ok((
1663 json_string(&created["outpoint"]["txid"])?,
1664 created["contract"].to_string(),
1665 ))
1666}
1667
1668async fn await_htlc_settled(client: &Client, operation_id: &Value) -> anyhow::Result<()> {
1669 assert_eq!(
1670 cmd!(
1671 client,
1672 "module",
1673 "lnv2",
1674 "htlc",
1675 "await-settled",
1676 json_string(operation_id)?
1677 )
1678 .out_json()
1679 .await?,
1680 "settled"
1681 );
1682
1683 Ok(())
1684}
1685
1686fn json_string(value: &Value) -> anyhow::Result<String> {
1687 value
1688 .as_str()
1689 .map(str::to_owned)
1690 .with_context(|| format!("Expected a JSON string, got {value}"))
1691}