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