1#![deny(clippy::pedantic)]
2
3use std::collections::BTreeMap;
4use std::fs::remove_dir_all;
5use std::ops::ControlFlow;
6use std::path::PathBuf;
7use std::str::FromStr;
8use std::time::Duration;
9use std::{env, ffi};
10
11use anyhow::ensure;
12use clap::{Parser, Subcommand};
13use devimint::cli::cleanup_on_exit;
14use devimint::envs::FM_DATA_DIR_ENV;
15use devimint::federation::Federation;
16use devimint::gatewayd::LdkChainSource;
17use devimint::util::{ProcessManager, poll, poll_with_timeout};
18use devimint::version_constants::{VERSION_0_6_0_ALPHA, VERSION_0_7_0_ALPHA};
19use devimint::{Gatewayd, LightningNode, cli, cmd, util};
20use fedimint_core::config::FederationId;
21use fedimint_core::time::now;
22use fedimint_core::util::backoff_util::aggressive_backoff_long;
23use fedimint_core::util::retry;
24use fedimint_core::{Amount, BitcoinAmountOrAll};
25use fedimint_gateway_common::{
26 FederationInfo, GatewayBalances, GatewayFedConfig, PaymentDetails, PaymentKind, PaymentStatus,
27};
28use fedimint_logging::LOG_TEST;
29use fedimint_testing_core::node_type::LightningNodeType;
30use itertools::Itertools;
31use tracing::{debug, info};
32
33#[derive(Parser)]
34struct GatewayTestOpts {
35 #[clap(subcommand)]
36 test: GatewayTest,
37}
38
39#[derive(Debug, Clone, Subcommand)]
40enum GatewayTest {
41 ConfigTest {
42 #[arg(long = "gw-type")]
43 gateway_type: LightningNodeType,
44 },
45 GatewaydMnemonic {
46 #[arg(long)]
47 old_gatewayd_path: PathBuf,
48 #[arg(long)]
49 new_gatewayd_path: PathBuf,
50 #[arg(long)]
51 old_gateway_cli_path: PathBuf,
52 #[arg(long)]
53 new_gateway_cli_path: PathBuf,
54 },
55 BackupRestoreTest,
56 LiquidityTest,
57 EsploraTest,
58}
59
60#[tokio::main]
61async fn main() -> anyhow::Result<()> {
62 let opts = GatewayTestOpts::parse();
63 match opts.test {
64 GatewayTest::ConfigTest { gateway_type } => Box::pin(config_test(gateway_type)).await,
65 GatewayTest::GatewaydMnemonic {
66 old_gatewayd_path,
67 new_gatewayd_path,
68 old_gateway_cli_path,
69 new_gateway_cli_path,
70 } => {
71 mnemonic_upgrade_test(
72 old_gatewayd_path,
73 new_gatewayd_path,
74 old_gateway_cli_path,
75 new_gateway_cli_path,
76 )
77 .await
78 }
79 GatewayTest::BackupRestoreTest => Box::pin(backup_restore_test()).await,
80 GatewayTest::LiquidityTest => Box::pin(liquidity_test()).await,
81 GatewayTest::EsploraTest => esplora_test().await,
82 }
83}
84
85async fn backup_restore_test() -> anyhow::Result<()> {
86 Box::pin(
87 devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
88 let gw = if devimint::util::supports_lnv2() {
89 dev_fed.gw_ldk_connected().await?
90 } else {
91 dev_fed.gw_lnd_registered().await?
92 };
93
94 let fed = dev_fed.fed().await?;
95 fed.pegin_gateways(10_000_000, vec![gw]).await?;
96
97 let mnemonic = gw.get_mnemonic().await?.mnemonic;
98
99 info!(target: LOG_TEST, "Wiping gateway and recovering without a backup...");
101 let ln = gw.ln.clone();
102 let new_gw = stop_and_recover_gateway(
103 process_mgr.clone(),
104 mnemonic.clone(),
105 gw.to_owned(),
106 ln.clone(),
107 fed,
108 )
109 .await?;
110
111 info!(target: LOG_TEST, "Wiping gateway and recovering with a backup...");
113 info!(target: LOG_TEST, "Creating backup...");
114 new_gw.backup_to_fed(fed).await?;
115 stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
116
117 info!(target: LOG_TEST, "backup_restore_test successful");
118 Ok(())
119 }),
120 )
121 .await
122}
123
124async fn stop_and_recover_gateway(
125 process_mgr: ProcessManager,
126 mnemonic: Vec<String>,
127 old_gw: Gatewayd,
128 new_ln: LightningNode,
129 fed: &Federation,
130) -> anyhow::Result<Gatewayd> {
131 let gateway_balances =
132 serde_json::from_value::<GatewayBalances>(cmd!(old_gw, "get-balances").out_json().await?)?;
133 let before_onchain_balance = gateway_balances.onchain_balance_sats;
134
135 let gw_type = old_gw.ln.ln_type();
137 let gw_name = old_gw.gw_name.clone();
138 old_gw.terminate().await?;
139 info!(target: LOG_TEST, "Terminated Gateway");
140
141 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
143 .expect("Data dir is not set")
144 .parse()
145 .expect("Could not parse data dir");
146 let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
147 remove_dir_all(gw_db)?;
148 info!(target: LOG_TEST, "Deleted the Gateway's database");
149
150 if gw_type == LightningNodeType::Ldk {
151 let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
153 remove_dir_all(ldk_data_dir)?;
154 info!(target: LOG_TEST, "Deleted LDK's database");
155 }
156
157 let seed = mnemonic.join(" ");
158 unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
160 let new_gw = Gatewayd::new(&process_mgr, new_ln).await?;
161 let new_mnemonic = new_gw.get_mnemonic().await?.mnemonic;
162 assert_eq!(mnemonic, new_mnemonic);
163 info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
164
165 let federations = serde_json::from_value::<Vec<FederationInfo>>(
166 new_gw.get_info().await?["federations"].clone(),
167 )?;
168 assert_eq!(0, federations.len());
169 info!(target: LOG_TEST, "Verified new Gateway has no federations");
170
171 new_gw.recover_fed(fed).await?;
172
173 let gateway_balances =
174 serde_json::from_value::<GatewayBalances>(cmd!(new_gw, "get-balances").out_json().await?)?;
175 let ecash_balance = gateway_balances
176 .ecash_balances
177 .first()
178 .expect("Should have one joined federation");
179 assert_eq!(
180 10_000_000,
181 ecash_balance.ecash_balance_msats.sats_round_down()
182 );
183 let after_onchain_balance = gateway_balances.onchain_balance_sats;
184 assert_eq!(before_onchain_balance, after_onchain_balance);
185 info!(target: LOG_TEST, "Verified balances after recovery");
186
187 Ok(new_gw)
188}
189
190async fn mnemonic_upgrade_test(
194 old_gatewayd_path: PathBuf,
195 new_gatewayd_path: PathBuf,
196 old_gateway_cli_path: PathBuf,
197 new_gateway_cli_path: PathBuf,
198) -> anyhow::Result<()> {
199 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
201 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
203 unsafe { std::env::set_var("FM_ENABLE_MODULE_LNV2", "0") };
205
206 devimint::run_devfed_test()
207 .call(|dev_fed, process_mgr| async move {
208 let gatewayd_version = util::Gatewayd::version_or_default().await;
209 let gateway_cli_version = util::GatewayCli::version_or_default().await;
210 info!(
211 target: LOG_TEST,
212 gatewayd_version = %gatewayd_version,
213 gateway_cli_version = %gateway_cli_version,
214 "Running gatewayd mnemonic test"
215 );
216
217 let mut gw_lnd = dev_fed.gw_lnd_registered().await?.to_owned();
218 let fed = dev_fed.fed().await?;
219 let federation_id = FederationId::from_str(fed.calculate_federation_id().as_str())?;
220
221 gw_lnd
222 .restart_with_bin(&process_mgr, &new_gatewayd_path, &new_gateway_cli_path)
223 .await?;
224
225 let mnemonic_response = gw_lnd.get_mnemonic().await?;
227 assert!(
228 mnemonic_response
229 .legacy_federations
230 .contains(&federation_id)
231 );
232
233 info!(target: LOG_TEST, "Verified a legacy federation exists");
234
235 gw_lnd.leave_federation(federation_id).await?;
237
238 gw_lnd.connect_fed(fed).await?;
240
241 let mnemonic_response = gw_lnd.get_mnemonic().await?;
243 assert!(
244 mnemonic_response
245 .legacy_federations
246 .contains(&federation_id)
247 );
248 assert_eq!(mnemonic_response.legacy_federations.len(), 1);
249
250 info!(target: LOG_TEST, "Verified leaving and re-joining preservers legacy federation");
251
252 gw_lnd.leave_federation(federation_id).await?;
254
255 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
256 .expect("Data dir is not set")
257 .parse()
258 .expect("Could not parse data dir");
259 let gw_fed_db = data_dir
260 .join(gw_lnd.gw_name.clone())
261 .join(format!("{federation_id}.db"));
262 remove_dir_all(gw_fed_db)?;
263
264 gw_lnd.connect_fed(fed).await?;
265
266 let mnemonic_response = gw_lnd.get_mnemonic().await?;
268 assert!(
269 !mnemonic_response
270 .legacy_federations
271 .contains(&federation_id)
272 );
273 assert_eq!(mnemonic_response.legacy_federations.len(), 0);
274
275 info!(target: LOG_TEST, "Verified deleting database will migrate the federation to use mnemonic");
276
277 info!(target: LOG_TEST, "Successfully completed mnemonic upgrade test");
278
279 Ok(())
280 })
281 .await
282}
283
284#[allow(clippy::too_many_lines)]
286async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
287 Box::pin(
288 devimint::run_devfed_test()
289 .num_feds(2)
290 .call(|dev_fed, process_mgr| async move {
291 let gw = match gw_type {
292 LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
293 LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
294 };
295
296 let invite_code = dev_fed.fed().await?.invite_code()?;
298 let output = cmd!(gw, "connect-fed", invite_code.clone())
299 .out_json()
300 .await;
301 assert!(
302 output.is_err(),
303 "Connecting to the same federation succeeded"
304 );
305 info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
306
307 let gatewayd_version = util::Gatewayd::version_or_default().await;
308
309 let fed_id = dev_fed.fed().await?.calculate_federation_id();
311 gw.set_federation_routing_fee(fed_id.clone(), 20, 20000)
312 .await?;
313
314 let lightning_fee = gw.get_lightning_fee(fed_id.clone()).await?;
315 assert_eq!(
316 lightning_fee.base.msats, 20,
317 "Federation base msat is not 20"
318 );
319 assert_eq!(
320 lightning_fee.parts_per_million, 20000,
321 "Federation proportional millionths is not 20000"
322 );
323 info!(target: LOG_TEST, "Verified per-federation routing fees changed");
324
325 let info_value = cmd!(gw, "info").out_json().await?;
326 let federations = info_value["federations"]
327 .as_array()
328 .expect("federations is an array");
329 assert_eq!(
330 federations.len(),
331 1,
332 "Gateway did not have one connected federation"
333 );
334
335 let config_val = if gatewayd_version < *VERSION_0_6_0_ALPHA {
337 cmd!(gw, "config", "--federation-id", fed_id)
338 .out_json()
339 .await?
340 } else {
341 cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
342 .out_json()
343 .await?
344 };
345
346 serde_json::from_value::<GatewayFedConfig>(config_val)?;
347
348 let bitcoind = dev_fed.bitcoind().await?;
350 let new_fed = Federation::new(
351 &process_mgr,
352 bitcoind.clone(),
353 false,
354 false,
355 1,
356 "config-test".to_string(),
357 )
358 .await?;
359 let new_fed_id = new_fed.calculate_federation_id();
360 info!(target: LOG_TEST, "Successfully spawned new federation");
361
362 let new_invite_code = new_fed.invite_code()?;
363 cmd!(gw, "connect-fed", new_invite_code.clone())
364 .out_json()
365 .await?;
366
367 let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_6_0_ALPHA {
368 (50000, 5000)
369 } else {
370 (0, 10000)
371 };
372
373 let lightning_fee = gw.get_lightning_fee(new_fed_id.clone()).await?;
374 assert_eq!(
375 lightning_fee.base.msats, default_base,
376 "Default Base msat for new federation was not correct"
377 );
378 assert_eq!(
379 lightning_fee.parts_per_million, default_ppm,
380 "Default Base msat for new federation was not correct"
381 );
382
383 info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
384
385 let pegin_amount = Amount::from_msats(10_000_000);
387 new_fed
388 .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
389 .await?;
390
391 let info_value = cmd!(gw, "info").out_json().await?;
393 let federations = info_value["federations"]
394 .as_array()
395 .expect("federations is an array");
396
397 assert_eq!(
398 federations.len(),
399 2,
400 "Gateway did not have two connected federations"
401 );
402
403 let federation_fake_scids =
404 serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
405 info_value
406 .get("channels")
407 .or_else(|| info_value.get("federation_fake_scids"))
408 .expect("field exists")
409 .to_owned(),
410 )
411 .expect("cannot parse")
412 .expect("should have scids");
413
414 assert_eq!(
415 federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
416 vec![1, 2]
417 );
418
419 let first_fed_info = federations
420 .iter()
421 .find(|i| {
422 *i["federation_id"]
423 .as_str()
424 .expect("should parse as str")
425 .to_string()
426 == fed_id
427 })
428 .expect("Could not find federation");
429
430 let second_fed_info = federations
431 .iter()
432 .find(|i| {
433 *i["federation_id"]
434 .as_str()
435 .expect("should parse as str")
436 .to_string()
437 == new_fed_id
438 })
439 .expect("Could not find federation");
440
441 let first_fed_balance_msat =
442 serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
443 .expect("fed should have balance");
444
445 let second_fed_balance_msat =
446 serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
447 .expect("fed should have balance");
448
449 assert_eq!(first_fed_balance_msat, Amount::ZERO);
450 assert_eq!(second_fed_balance_msat, pegin_amount);
451
452 leave_federation(gw, fed_id, 1).await?;
453 leave_federation(gw, new_fed_id, 2).await?;
454
455 let output = cmd!(gw, "connect-fed", new_invite_code.clone())
457 .out_json()
458 .await?;
459 let rejoined_federation_balance_msat =
460 serde_json::from_value::<Amount>(output["balance_msat"].clone())
461 .expect("fed has balance");
462
463 assert_eq!(second_fed_balance_msat, rejoined_federation_balance_msat);
464
465 info!(target: LOG_TEST, "Gateway configuration test successful");
466 Ok(())
467 }),
468 )
469 .await
470}
471
472#[allow(clippy::too_many_lines)]
475async fn liquidity_test() -> anyhow::Result<()> {
476 devimint::run_devfed_test()
477 .call(|dev_fed, _process_mgr| async move {
478 let federation = dev_fed.fed().await?;
479
480 if !devimint::util::supports_lnv2() {
481 info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
482 return Ok(());
483 }
484
485 let gw_lnd = dev_fed.gw_lnd_registered().await?;
486 let gw_ldk = dev_fed.gw_ldk_connected().await?;
487 let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
488 let gateways = [gw_lnd, gw_ldk].to_vec();
489
490 let gateway_matrix = gateways
491 .iter()
492 .cartesian_product(gateways.iter())
493 .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
494
495 info!(target: LOG_TEST, "Pegging-in gateways...");
496 federation
497 .pegin_gateways(1_000_000, gateways.clone())
498 .await?;
499
500 info!(target: LOG_TEST, "Testing ecash payments between gateways...");
501 for (gw_send, gw_receive) in gateway_matrix.clone() {
502 info!(
503 target: LOG_TEST,
504 gw_send = %gw_send.ln.ln_type(),
505 gw_receive = %gw_receive.ln.ln_type(),
506 "Testing ecash payment",
507 );
508
509 let fed_id = federation.calculate_federation_id();
510 let prev_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
511 let prev_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
512 let ecash = gw_send.send_ecash(fed_id.clone(), 500_000).await?;
513 gw_receive.receive_ecash(ecash).await?;
514 let after_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
515 let after_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
516 assert_eq!(prev_send_ecash_balance - 500_000, after_send_ecash_balance);
517 assert_eq!(
518 prev_receive_ecash_balance + 500_000,
519 after_receive_ecash_balance
520 );
521 }
522
523 info!(target: LOG_TEST, "Testing payments between gateways...");
524 for (gw_send, gw_receive) in gateway_matrix.clone() {
525 info!(
526 target: LOG_TEST,
527 gw_send = %gw_send.ln.ln_type(),
528 gw_receive = %gw_receive.ln.ln_type(),
529 "Testing lightning payment",
530 );
531
532 let invoice = gw_receive.create_invoice(1_000_000).await?;
533 gw_send.pay_invoice(invoice).await?;
534 }
535
536 if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
537 let start = now() - Duration::from_secs(5 * 60);
538 let end = now() + Duration::from_secs(5 * 60);
539 info!(target: LOG_TEST, "Verifying list of transactions");
540 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
541 assert_eq!(lnd_transactions.len(), 2);
543
544 let ldk_transactions = gw_ldk.list_transactions(start, end).await?;
545 assert_eq!(ldk_transactions.len(), 2);
546
547 let start = now() - Duration::from_secs(10 * 60);
549 let end = now() - Duration::from_secs(5 * 60);
550 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
551 assert_eq!(lnd_transactions.len(), 0);
552 }
553
554 info!(target: LOG_TEST, "Testing paying through LND Gateway...");
555 poll_with_timeout("LDK2 pay LDK", Duration::from_secs(180), || async {
559 debug!(target: LOG_TEST, "Trying LDK2 -> LND -> LDK...");
560 let invoice = gw_ldk
561 .create_invoice(1_550_000)
562 .await
563 .map_err(ControlFlow::Continue)?;
564 gw_ldk_second
565 .pay_invoice(invoice.clone())
566 .await
567 .map_err(ControlFlow::Continue)?;
568 Ok(())
569 })
570 .await?;
571
572 if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
573 info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
574 poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
576 let offer_with_amount = gw_ldk_second.create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
577 gw_ldk.pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
578 assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
579 Ok(())
580 }).await?;
581
582 let offer_without_amount = gw_ldk.create_offer(None).await?;
583 gw_ldk_second.pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
584 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
585
586 gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
588
589 gw_ldk_second.pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
591 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
592 }
593
594 info!(target: LOG_TEST, "Pegging-out gateways...");
595 federation
596 .pegout_gateways(500_000_000, gateways.clone())
597 .await?;
598
599 info!(target: LOG_TEST, "Testing closing all channels...");
600 gw_ldk_second.close_all_channels().await?;
601 gw_ldk.close_all_channels().await?;
602 let bitcoind = dev_fed.bitcoind().await?;
603 bitcoind.mine_blocks(2016).await?;
605 let block_height = bitcoind.get_block_count().await? - 1;
606 gw_ldk_second.wait_for_block_height(block_height).await?;
607 check_empty_lightning_balance(gw_ldk_second).await?;
608 gw_ldk.wait_for_block_height(block_height).await?;
609 check_empty_lightning_balance(gw_ldk).await?;
610 check_empty_lightning_balance(gw_lnd).await?;
611
612 info!(target: LOG_TEST, "Testing sending onchain...");
613 gw_ldk
616 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
617 .await?;
618 gw_ldk_second
619 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
620 .await?;
621 check_empty_onchain_balance(gw_ldk).await?;
622 check_empty_onchain_balance(gw_ldk_second).await?;
623
624 Ok(())
625 })
626 .await
627}
628
629async fn esplora_test() -> anyhow::Result<()> {
632 let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
633 let (process_mgr, task_group) = cli::setup(args).await?;
634 cleanup_on_exit(
635 async {
636 unsafe {
638 std::env::set_var("FM_GATEWAY_NETWORK", "signet");
639 std::env::set_var("FM_LDK_NETWORK", "signet");
640 }
641 let ldk = Gatewayd::new(
642 &process_mgr,
643 LightningNode::Ldk {
644 name: "gateway-ldk-mutinynet".to_string(),
645 gw_port: process_mgr.globals.FM_PORT_GW_LDK,
646 ldk_port: process_mgr.globals.FM_PORT_LDK,
647 chain_source: LdkChainSource::Esplora,
648 },
649 )
650 .await?;
651
652 poll("Waiting for LDK to be ready", || async {
653 let info = ldk.get_info().await.map_err(ControlFlow::Continue)?;
654 let state: String = serde_json::from_value(info["gateway_state"].clone())
655 .expect("Could not get gateway state");
656 if state == "Running" {
657 Ok(())
658 } else {
659 Err(ControlFlow::Continue(anyhow::anyhow!(
660 "Gateway not running"
661 )))
662 }
663 })
664 .await?;
665
666 ldk.get_ln_onchain_address().await?;
667 info!(target:LOG_TEST, "Successfully connected to mutinynet esplora");
668 Ok(())
669 },
670 task_group,
671 )
672 .await?;
673 Ok(())
674}
675
676async fn get_transaction(
677 gateway: &Gatewayd,
678 kind: PaymentKind,
679 amount: Amount,
680 status: PaymentStatus,
681) -> Option<PaymentDetails> {
682 let transactions = gateway
683 .list_transactions(
684 now() - Duration::from_secs(5 * 60),
685 now() + Duration::from_secs(5 * 60),
686 )
687 .await
688 .ok()?;
689 transactions.into_iter().find(|details| {
690 details.payment_kind == kind && details.amount == amount && details.status == status
691 })
692}
693
694async fn check_empty_onchain_balance(gw: &Gatewayd) -> anyhow::Result<()> {
695 retry(
696 "Wait for onchain balance update",
697 aggressive_backoff_long(),
698 || async {
699 let curr_balance = gw.get_balances().await?.onchain_balance_sats;
700 let gw_name = gw.gw_name.clone();
701 ensure!(
702 curr_balance == 0,
703 "Gateway onchain balance is not empty: {curr_balance} gw_name: {gw_name}"
704 );
705 Ok(())
706 },
707 )
708 .await
709}
710
711async fn check_empty_lightning_balance(gw: &Gatewayd) -> anyhow::Result<()> {
712 let balances = gw.get_balances().await?;
713 let curr_lightning_balance = balances.lightning_balance_msats;
714 ensure!(
715 curr_lightning_balance == 0,
716 "Close channels did not sweep all lightning funds"
717 );
718 let inbound_lightning_balance = balances.inbound_lightning_liquidity_msats;
719 ensure!(
720 inbound_lightning_balance == 0,
721 "Close channels did not sweep all lightning funds"
722 );
723 Ok(())
724}
725
726async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
729 let gatewayd_version = util::Gatewayd::version_or_default().await;
730 let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
731 .out_json()
732 .await
733 .expect("Leaving the federation failed");
734
735 let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
736 assert_eq!(federation_id.to_string(), fed_id);
737
738 let scid = if gatewayd_version < *VERSION_0_6_0_ALPHA {
739 serde_json::from_value::<u64>(leave_fed["federation_index"].clone())?
740 } else {
741 serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?
742 };
743
744 assert_eq!(scid, expected_scid);
745
746 info!(target: LOG_TEST, federation_id = %fed_id, "Verified gateway left federation");
747 Ok(())
748}