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 let old_gw_index = old_gw.gateway_index;
136 old_gw.terminate().await?;
137 info!(target: LOG_TEST, "Terminated Gateway");
138
139 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
141 .expect("Data dir is not set")
142 .parse()
143 .expect("Could not parse data dir");
144 let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
145 if gw_db.is_file() {
146 remove_file(gw_db)?;
148 } else {
149 remove_dir_all(gw_db)?;
150 }
151 info!(target: LOG_TEST, "Deleted the Gateway's database");
152
153 if gw_type == LightningNodeType::Ldk {
154 let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
156 remove_dir_all(ldk_data_dir)?;
157 info!(target: LOG_TEST, "Deleted LDK's database");
158 }
159
160 let seed = mnemonic.join(" ");
161 unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
163 let new_gw = Gatewayd::new(&process_mgr, new_ln, old_gw_index).await?;
164 let new_mnemonic = new_gw.get_mnemonic().await?.mnemonic;
165 assert_eq!(mnemonic, new_mnemonic);
166 info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
167
168 let federations = serde_json::from_value::<Vec<FederationInfo>>(
169 new_gw.get_info().await?["federations"].clone(),
170 )?;
171 assert_eq!(0, federations.len());
172 info!(target: LOG_TEST, "Verified new Gateway has no federations");
173
174 new_gw.recover_fed(fed).await?;
175
176 let gateway_balances =
177 serde_json::from_value::<GatewayBalances>(cmd!(new_gw, "get-balances").out_json().await?)?;
178 let ecash_balance = gateway_balances
179 .ecash_balances
180 .first()
181 .expect("Should have one joined federation");
182 almost_equal(
183 ecash_balance.ecash_balance_msats.sats_round_down(),
184 10_000_000,
185 10,
186 )
187 .unwrap();
188 let after_onchain_balance = gateway_balances.onchain_balance_sats;
189 assert_eq!(before_onchain_balance, after_onchain_balance);
190 info!(target: LOG_TEST, "Verified balances after recovery");
191
192 Ok(new_gw)
193}
194
195async fn mnemonic_upgrade_test(
199 old_gatewayd_path: PathBuf,
200 new_gatewayd_path: PathBuf,
201 old_gateway_cli_path: PathBuf,
202 new_gateway_cli_path: PathBuf,
203) -> anyhow::Result<()> {
204 unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
206 unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
208 unsafe { std::env::set_var("FM_ENABLE_MODULE_LNV2", "0") };
210
211 devimint::run_devfed_test()
212 .call(|dev_fed, process_mgr| async move {
213 let gatewayd_version = util::Gatewayd::version_or_default().await;
214 let gateway_cli_version = util::GatewayCli::version_or_default().await;
215 info!(
216 target: LOG_TEST,
217 gatewayd_version = %gatewayd_version,
218 gateway_cli_version = %gateway_cli_version,
219 "Running gatewayd mnemonic test"
220 );
221
222 let mut gw_lnd = dev_fed.gw_lnd_registered().await?.to_owned();
223 let fed = dev_fed.fed().await?;
224 let federation_id = FederationId::from_str(fed.calculate_federation_id().as_str())?;
225
226 gw_lnd
227 .restart_with_bin(&process_mgr, &new_gatewayd_path, &new_gateway_cli_path)
228 .await?;
229
230 let mnemonic_response = gw_lnd.get_mnemonic().await?;
232 assert!(
233 mnemonic_response
234 .legacy_federations
235 .contains(&federation_id)
236 );
237
238 info!(target: LOG_TEST, "Verified a legacy federation exists");
239
240 gw_lnd.leave_federation(federation_id).await?;
242
243 gw_lnd.connect_fed(fed).await?;
245
246 let mnemonic_response = gw_lnd.get_mnemonic().await?;
248 assert!(
249 mnemonic_response
250 .legacy_federations
251 .contains(&federation_id)
252 );
253 assert_eq!(mnemonic_response.legacy_federations.len(), 1);
254
255 info!(target: LOG_TEST, "Verified leaving and re-joining preservers legacy federation");
256
257 gw_lnd.leave_federation(federation_id).await?;
259
260 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
261 .expect("Data dir is not set")
262 .parse()
263 .expect("Could not parse data dir");
264 let gw_fed_db = data_dir
265 .join(gw_lnd.gw_name.clone())
266 .join(format!("{federation_id}.db"));
267 remove_dir_all(gw_fed_db)?;
268
269 gw_lnd.connect_fed(fed).await?;
270
271 let mnemonic_response = gw_lnd.get_mnemonic().await?;
273 assert!(
274 !mnemonic_response
275 .legacy_federations
276 .contains(&federation_id)
277 );
278 assert_eq!(mnemonic_response.legacy_federations.len(), 0);
279
280 info!(target: LOG_TEST, "Verified deleting database will migrate the federation to use mnemonic");
281
282 info!(target: LOG_TEST, "Successfully completed mnemonic upgrade test");
283
284 Ok(())
285 })
286 .await
287}
288
289#[allow(clippy::too_many_lines)]
291async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
292 Box::pin(
293 devimint::run_devfed_test()
294 .num_feds(2)
295 .call(|dev_fed, process_mgr| async move {
296 let gw = match gw_type {
297 LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
298 LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
299 };
300
301 let invite_code = dev_fed.fed().await?.invite_code()?;
303 let output = cmd!(gw, "connect-fed", invite_code.clone())
304 .out_json()
305 .await;
306 assert!(
307 output.is_err(),
308 "Connecting to the same federation succeeded"
309 );
310 info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
311
312 let gatewayd_version = util::Gatewayd::version_or_default().await;
313
314 let fed_id = dev_fed.fed().await?.calculate_federation_id();
316 gw.set_federation_routing_fee(fed_id.clone(), 20, 20000)
317 .await?;
318
319 let lightning_fee = gw.get_lightning_fee(fed_id.clone()).await?;
320 assert_eq!(
321 lightning_fee.base.msats, 20,
322 "Federation base msat is not 20"
323 );
324 assert_eq!(
325 lightning_fee.parts_per_million, 20000,
326 "Federation proportional millionths is not 20000"
327 );
328 info!(target: LOG_TEST, "Verified per-federation routing fees changed");
329
330 let info_value = cmd!(gw, "info").out_json().await?;
331 let federations = info_value["federations"]
332 .as_array()
333 .expect("federations is an array");
334 assert_eq!(
335 federations.len(),
336 1,
337 "Gateway did not have one connected federation"
338 );
339
340 let config_val = cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
342 .out_json()
343 .await?;
344
345 serde_json::from_value::<GatewayFedConfig>(config_val)?;
346
347 let bitcoind = dev_fed.bitcoind().await?;
349 let new_fed = Federation::new(
350 &process_mgr,
351 bitcoind.clone(),
352 false,
353 false,
354 1,
355 "config-test".to_string(),
356 )
357 .await?;
358 let new_fed_id = new_fed.calculate_federation_id();
359 info!(target: LOG_TEST, "Successfully spawned new federation");
360
361 let new_invite_code = new_fed.invite_code()?;
362 cmd!(gw, "connect-fed", new_invite_code.clone())
363 .out_json()
364 .await?;
365
366
367 let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_8_0_ALPHA {
368 (0, 0)
369 } else {
370 (50000, 5000)
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 almost_equal(second_fed_balance_msat.msats, pegin_amount.msats, 10_000).unwrap();
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 if gw.gatewayd_version >= *VERSION_0_10_0_ALPHA {
466 info!(target: LOG_TEST, gatewayd_version = %gw.gatewayd_version, "Getting info over iroh");
468 gw.get_info_iroh().await?;
469 }
470
471 info!(target: LOG_TEST, "Gateway configuration test successful");
472 Ok(())
473 }),
474 )
475 .await
476}
477
478#[allow(clippy::too_many_lines)]
481async fn liquidity_test() -> anyhow::Result<()> {
482 devimint::run_devfed_test()
483 .call(|dev_fed, _process_mgr| async move {
484 let federation = dev_fed.fed().await?;
485
486 if !devimint::util::supports_lnv2() {
487 info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
488 return Ok(());
489 }
490
491 let gw_lnd = dev_fed.gw_lnd_registered().await?;
492 let gw_ldk = dev_fed.gw_ldk_connected().await?;
493 let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
494 let gateways = [gw_lnd, gw_ldk].to_vec();
495
496 let gateway_matrix = gateways
497 .iter()
498 .cartesian_product(gateways.iter())
499 .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
500
501 info!(target: LOG_TEST, "Pegging-in gateways...");
502 federation
503 .pegin_gateways(1_000_000, gateways.clone())
504 .await?;
505
506 info!(target: LOG_TEST, "Testing ecash payments between gateways...");
507 for (gw_send, gw_receive) in gateway_matrix.clone() {
508 info!(
509 target: LOG_TEST,
510 gw_send = %gw_send.ln.ln_type(),
511 gw_receive = %gw_receive.ln.ln_type(),
512 "Testing ecash payment",
513 );
514
515 let fed_id = federation.calculate_federation_id();
516 let prev_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
517 let prev_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
518 let ecash = gw_send.send_ecash(fed_id.clone(), 500_000).await?;
519 gw_receive.receive_ecash(ecash).await?;
520 let after_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
521 let after_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
522 assert_eq!(prev_send_ecash_balance - 500_000, after_send_ecash_balance);
523 almost_equal(
524 prev_receive_ecash_balance + 500_000,
525 after_receive_ecash_balance,
526 2_000,
527 )
528 .unwrap();
529 }
530
531 info!(target: LOG_TEST, "Testing payments between gateways...");
532 for (gw_send, gw_receive) in gateway_matrix.clone() {
533 info!(
534 target: LOG_TEST,
535 gw_send = %gw_send.ln.ln_type(),
536 gw_receive = %gw_receive.ln.ln_type(),
537 "Testing lightning payment",
538 );
539
540 let invoice = gw_receive.create_invoice(1_000_000).await?;
541 gw_send.pay_invoice(invoice).await?;
542 }
543
544 let start = now() - Duration::from_secs(5 * 60);
545 let end = now() + Duration::from_secs(5 * 60);
546 info!(target: LOG_TEST, "Verifying list of transactions");
547 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
548 assert_eq!(lnd_transactions.len(), 2);
550
551 let ldk_transactions = gw_ldk.list_transactions(start, end).await?;
552 assert_eq!(ldk_transactions.len(), 2);
553
554 let start = now() - Duration::from_secs(10 * 60);
556 let end = now() - Duration::from_secs(5 * 60);
557 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
558 assert_eq!(lnd_transactions.len(), 0);
559
560 info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
561 poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
563 let offer_with_amount = gw_ldk_second.create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
564 gw_ldk.pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
565 assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
566 Ok(())
567 }).await?;
568
569 let offer_without_amount = gw_ldk.create_offer(None).await?;
570 gw_ldk_second.pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
571 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
572
573 gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
575
576 gw_ldk_second.pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
578 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
579
580 info!(target: LOG_TEST, "Pegging-out gateways...");
581 federation
582 .pegout_gateways(500_000_000, gateways.clone())
583 .await?;
584
585 info!(target: LOG_TEST, "Testing sending onchain...");
586 let bitcoind = dev_fed.bitcoind().await?;
587 for gw in &gateways {
588 let txid = gw
589 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
590 .await?;
591 bitcoind.poll_get_transaction(txid).await?;
592 }
593
594 info!(target: LOG_TEST, "Testing closing all channels...");
595
596 let gw_ldk_pubkey = gw_ldk.lightning_pubkey().await?;
598 gw_lnd.close_channel(gw_ldk_pubkey, false).await?;
599
600 gw_ldk_second.close_all_channels(true).await?;
602
603 for gw in gateways {
605 let channels = gw.list_channels().await?;
606 let active_channel = channels.into_iter().any(|chan| chan.is_active);
607 assert!(!active_channel);
608 }
609
610 Ok(())
611 })
612 .await
613}
614
615async fn esplora_test() -> anyhow::Result<()> {
616 let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
617 let (process_mgr, task_group) = cli::setup(args).await?;
618 cleanup_on_exit(
619 async {
620 info!("Spawning bitcoind...");
621 let bitcoind = Bitcoind::new(&process_mgr, false).await?;
622 info!("Spawning esplora...");
623 let _esplora = Esplora::new(&process_mgr, bitcoind).await?;
624 let network = bitcoin::Network::from_str(&process_mgr.globals.FM_GATEWAY_NETWORK)
625 .expect("Could not parse network");
626 let esplora_port = process_mgr.globals.FM_PORT_ESPLORA.to_string();
627 let esplora = default_esplora_server(network, Some(esplora_port));
628 unsafe {
629 std::env::remove_var("FM_BITCOIND_URL");
630 std::env::set_var("FM_ESPLORA_URL", esplora.url.to_string());
631 }
632 info!("Spawning ldk gateway...");
633 let ldk = Gatewayd::new(
634 &process_mgr,
635 LightningNode::Ldk {
636 name: "gateway-ldk-esplora".to_string(),
637 gw_port: process_mgr.globals.FM_PORT_GW_LDK,
638 ldk_port: process_mgr.globals.FM_PORT_LDK,
639 },
640 0,
641 )
642 .await?;
643
644 info!("Waiting for ldk gatewy to be ready...");
645 poll("Waiting for LDK to be ready", || async {
646 let info = ldk.get_info().await.map_err(ControlFlow::Continue)?;
647 let state: String = serde_json::from_value(info["gateway_state"].clone())
648 .expect("Could not get gateway state");
649 if state == "Running" {
650 Ok(())
651 } else {
652 Err(ControlFlow::Continue(anyhow::anyhow!(
653 "Gateway not running"
654 )))
655 }
656 })
657 .await?;
658
659 ldk.get_ln_onchain_address().await?;
660 info!(target:LOG_TEST, "ldk gateway successfully spawned and connected to esplora");
661 Ok(())
662 },
663 task_group,
664 )
665 .await?;
666 Ok(())
667}
668
669async fn get_transaction(
670 gateway: &Gatewayd,
671 kind: PaymentKind,
672 amount: Amount,
673 status: PaymentStatus,
674) -> Option<PaymentDetails> {
675 let transactions = gateway
676 .list_transactions(
677 now() - Duration::from_secs(5 * 60),
678 now() + Duration::from_secs(5 * 60),
679 )
680 .await
681 .ok()?;
682 transactions.into_iter().find(|details| {
683 details.payment_kind == kind && details.amount == amount && details.status == status
684 })
685}
686
687async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
690 let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
691 .out_json()
692 .await
693 .expect("Leaving the federation failed");
694
695 let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
696 assert_eq!(federation_id.to_string(), fed_id);
697
698 let scid = serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?;
699
700 assert_eq!(scid, expected_scid);
701
702 info!(target: LOG_TEST, federation_id = %fed_id, "Verified gateway left federation");
703 Ok(())
704}