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 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 if gw_db.is_file() {
148 remove_file(gw_db)?;
150 } else {
151 remove_dir_all(gw_db)?;
152 }
153 info!(target: LOG_TEST, "Deleted the Gateway's database");
154
155 if gw_type == LightningNodeType::Ldk {
156 let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
158 remove_dir_all(ldk_data_dir)?;
159 info!(target: LOG_TEST, "Deleted LDK's database");
160 }
161
162 let seed = mnemonic.join(" ");
163 unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
165 let new_gw = Gatewayd::new(&process_mgr, new_ln).await?;
166 let new_mnemonic = new_gw.get_mnemonic().await?.mnemonic;
167 assert_eq!(mnemonic, new_mnemonic);
168 info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
169
170 let federations = serde_json::from_value::<Vec<FederationInfo>>(
171 new_gw.get_info().await?["federations"].clone(),
172 )?;
173 assert_eq!(0, federations.len());
174 info!(target: LOG_TEST, "Verified new Gateway has no federations");
175
176 new_gw.recover_fed(fed).await?;
177
178 let gateway_balances =
179 serde_json::from_value::<GatewayBalances>(cmd!(new_gw, "get-balances").out_json().await?)?;
180 let ecash_balance = gateway_balances
181 .ecash_balances
182 .first()
183 .expect("Should have one joined federation");
184 assert_eq!(
185 10_000_000,
186 ecash_balance.ecash_balance_msats.sats_round_down()
187 );
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 = if gatewayd_version < *VERSION_0_6_0_ALPHA {
342 cmd!(gw, "config", "--federation-id", fed_id)
343 .out_json()
344 .await?
345 } else {
346 cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
347 .out_json()
348 .await?
349 };
350
351 serde_json::from_value::<GatewayFedConfig>(config_val)?;
352
353 let bitcoind = dev_fed.bitcoind().await?;
355 let new_fed = Federation::new(
356 &process_mgr,
357 bitcoind.clone(),
358 false,
359 false,
360 1,
361 "config-test".to_string(),
362 )
363 .await?;
364 let new_fed_id = new_fed.calculate_federation_id();
365 info!(target: LOG_TEST, "Successfully spawned new federation");
366
367 let new_invite_code = new_fed.invite_code()?;
368 cmd!(gw, "connect-fed", new_invite_code.clone())
369 .out_json()
370 .await?;
371
372 let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_6_0_ALPHA {
373 (50000, 5000)
374 } else {
375 (0, 10000)
376 };
377
378 let lightning_fee = gw.get_lightning_fee(new_fed_id.clone()).await?;
379 assert_eq!(
380 lightning_fee.base.msats, default_base,
381 "Default Base msat for new federation was not correct"
382 );
383 assert_eq!(
384 lightning_fee.parts_per_million, default_ppm,
385 "Default Base msat for new federation was not correct"
386 );
387
388 info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
389
390 let pegin_amount = Amount::from_msats(10_000_000);
392 new_fed
393 .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
394 .await?;
395
396 let info_value = cmd!(gw, "info").out_json().await?;
398 let federations = info_value["federations"]
399 .as_array()
400 .expect("federations is an array");
401
402 assert_eq!(
403 federations.len(),
404 2,
405 "Gateway did not have two connected federations"
406 );
407
408 let federation_fake_scids =
409 serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
410 info_value
411 .get("channels")
412 .or_else(|| info_value.get("federation_fake_scids"))
413 .expect("field exists")
414 .to_owned(),
415 )
416 .expect("cannot parse")
417 .expect("should have scids");
418
419 assert_eq!(
420 federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
421 vec![1, 2]
422 );
423
424 let first_fed_info = federations
425 .iter()
426 .find(|i| {
427 *i["federation_id"]
428 .as_str()
429 .expect("should parse as str")
430 .to_string()
431 == fed_id
432 })
433 .expect("Could not find federation");
434
435 let second_fed_info = federations
436 .iter()
437 .find(|i| {
438 *i["federation_id"]
439 .as_str()
440 .expect("should parse as str")
441 .to_string()
442 == new_fed_id
443 })
444 .expect("Could not find federation");
445
446 let first_fed_balance_msat =
447 serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
448 .expect("fed should have balance");
449
450 let second_fed_balance_msat =
451 serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
452 .expect("fed should have balance");
453
454 assert_eq!(first_fed_balance_msat, Amount::ZERO);
455 assert_eq!(second_fed_balance_msat, pegin_amount);
456
457 leave_federation(gw, fed_id, 1).await?;
458 leave_federation(gw, new_fed_id, 2).await?;
459
460 let output = cmd!(gw, "connect-fed", new_invite_code.clone())
462 .out_json()
463 .await?;
464 let rejoined_federation_balance_msat =
465 serde_json::from_value::<Amount>(output["balance_msat"].clone())
466 .expect("fed has balance");
467
468 assert_eq!(second_fed_balance_msat, rejoined_federation_balance_msat);
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 assert_eq!(
523 prev_receive_ecash_balance + 500_000,
524 after_receive_ecash_balance
525 );
526 }
527
528 info!(target: LOG_TEST, "Testing payments between gateways...");
529 for (gw_send, gw_receive) in gateway_matrix.clone() {
530 info!(
531 target: LOG_TEST,
532 gw_send = %gw_send.ln.ln_type(),
533 gw_receive = %gw_receive.ln.ln_type(),
534 "Testing lightning payment",
535 );
536
537 let invoice = gw_receive.create_invoice(1_000_000).await?;
538 gw_send.pay_invoice(invoice).await?;
539 }
540
541 if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
542 let start = now() - Duration::from_secs(5 * 60);
543 let end = now() + Duration::from_secs(5 * 60);
544 info!(target: LOG_TEST, "Verifying list of transactions");
545 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
546 assert_eq!(lnd_transactions.len(), 2);
548
549 let ldk_transactions = gw_ldk.list_transactions(start, end).await?;
550 assert_eq!(ldk_transactions.len(), 2);
551
552 let start = now() - Duration::from_secs(10 * 60);
554 let end = now() - Duration::from_secs(5 * 60);
555 let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
556 assert_eq!(lnd_transactions.len(), 0);
557 }
558
559 info!(target: LOG_TEST, "Testing paying through LND Gateway...");
560 poll_with_timeout("LDK2 pay LDK", Duration::from_secs(180), || async {
564 debug!(target: LOG_TEST, "Trying LDK2 -> LND -> LDK...");
565 let invoice = gw_ldk
566 .create_invoice(1_550_000)
567 .await
568 .map_err(ControlFlow::Continue)?;
569 gw_ldk_second
570 .pay_invoice(invoice.clone())
571 .await
572 .map_err(ControlFlow::Continue)?;
573 Ok(())
574 })
575 .await?;
576
577 if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
578 info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
579 poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
581 let offer_with_amount = gw_ldk_second.create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
582 gw_ldk.pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
583 assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
584 Ok(())
585 }).await?;
586
587 let offer_without_amount = gw_ldk.create_offer(None).await?;
588 gw_ldk_second.pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
589 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
590
591 gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
593
594 gw_ldk_second.pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
596 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
597 }
598
599 info!(target: LOG_TEST, "Pegging-out gateways...");
600 federation
601 .pegout_gateways(500_000_000, gateways.clone())
602 .await?;
603
604 info!(target: LOG_TEST, "Testing closing all channels...");
605 gw_ldk_second.close_all_channels().await?;
606 gw_ldk.close_all_channels().await?;
607 let bitcoind = dev_fed.bitcoind().await?;
608 bitcoind.mine_blocks(2016).await?;
610 let block_height = bitcoind.get_block_count().await? - 1;
611 gw_ldk_second.wait_for_block_height(block_height).await?;
612 check_empty_lightning_balance(gw_ldk_second).await?;
613 gw_ldk.wait_for_block_height(block_height).await?;
614 check_empty_lightning_balance(gw_ldk).await?;
615 check_empty_lightning_balance(gw_lnd).await?;
616
617 info!(target: LOG_TEST, "Testing sending onchain...");
618 gw_ldk
621 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
622 .await?;
623 gw_ldk_second
624 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
625 .await?;
626 check_empty_onchain_balance(gw_ldk).await?;
627 check_empty_onchain_balance(gw_ldk_second).await?;
628
629 Ok(())
630 })
631 .await
632}
633
634async fn esplora_test() -> anyhow::Result<()> {
637 let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
638 let (process_mgr, task_group) = cli::setup(args).await?;
639 cleanup_on_exit(
640 async {
641 unsafe {
643 std::env::set_var("FM_GATEWAY_NETWORK", "signet");
644 std::env::set_var("FM_LDK_NETWORK", "signet");
645 }
646 let ldk = Gatewayd::new(
647 &process_mgr,
648 LightningNode::Ldk {
649 name: "gateway-ldk-mutinynet".to_string(),
650 gw_port: process_mgr.globals.FM_PORT_GW_LDK,
651 ldk_port: process_mgr.globals.FM_PORT_LDK,
652 chain_source: LdkChainSource::Esplora,
653 },
654 )
655 .await?;
656
657 poll("Waiting for LDK to be ready", || async {
658 let info = ldk.get_info().await.map_err(ControlFlow::Continue)?;
659 let state: String = serde_json::from_value(info["gateway_state"].clone())
660 .expect("Could not get gateway state");
661 if state == "Running" {
662 Ok(())
663 } else {
664 Err(ControlFlow::Continue(anyhow::anyhow!(
665 "Gateway not running"
666 )))
667 }
668 })
669 .await?;
670
671 ldk.get_ln_onchain_address().await?;
672 info!(target:LOG_TEST, "Successfully connected to mutinynet esplora");
673 Ok(())
674 },
675 task_group,
676 )
677 .await?;
678 Ok(())
679}
680
681async fn get_transaction(
682 gateway: &Gatewayd,
683 kind: PaymentKind,
684 amount: Amount,
685 status: PaymentStatus,
686) -> Option<PaymentDetails> {
687 let transactions = gateway
688 .list_transactions(
689 now() - Duration::from_secs(5 * 60),
690 now() + Duration::from_secs(5 * 60),
691 )
692 .await
693 .ok()?;
694 transactions.into_iter().find(|details| {
695 details.payment_kind == kind && details.amount == amount && details.status == status
696 })
697}
698
699async fn check_empty_onchain_balance(gw: &Gatewayd) -> anyhow::Result<()> {
700 retry(
701 "Wait for onchain balance update",
702 aggressive_backoff_long(),
703 || async {
704 let curr_balance = gw.get_balances().await?.onchain_balance_sats;
705 let gw_name = gw.gw_name.clone();
706 ensure!(
707 curr_balance == 0,
708 "Gateway onchain balance is not empty: {curr_balance} gw_name: {gw_name}"
709 );
710 Ok(())
711 },
712 )
713 .await
714}
715
716async fn check_empty_lightning_balance(gw: &Gatewayd) -> anyhow::Result<()> {
717 let balances = gw.get_balances().await?;
718 let curr_lightning_balance = balances.lightning_balance_msats;
719 ensure!(
720 curr_lightning_balance == 0,
721 "Close channels did not sweep all lightning funds"
722 );
723 let inbound_lightning_balance = balances.inbound_lightning_liquidity_msats;
724 ensure!(
725 inbound_lightning_balance == 0,
726 "Close channels did not sweep all lightning funds"
727 );
728 Ok(())
729}
730
731async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
734 let gatewayd_version = util::Gatewayd::version_or_default().await;
735 let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
736 .out_json()
737 .await
738 .expect("Leaving the federation failed");
739
740 let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
741 assert_eq!(federation_id.to_string(), fed_id);
742
743 let scid = if gatewayd_version < *VERSION_0_6_0_ALPHA {
744 serde_json::from_value::<u64>(leave_fed["federation_index"].clone())?
745 } else {
746 serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?
747 };
748
749 assert_eq!(scid, expected_scid);
750
751 info!(target: LOG_TEST, federation_id = %fed_id, "Verified gateway left federation");
752 Ok(())
753}