gateway_tests/
main.rs

1#![deny(clippy::pedantic)]
2
3use std::collections::BTreeMap;
4use std::env;
5use std::fs::remove_dir_all;
6use std::ops::ControlFlow;
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::time::Duration;
10
11use anyhow::ensure;
12use clap::{Parser, Subcommand};
13use devimint::envs::FM_DATA_DIR_ENV;
14use devimint::federation::Federation;
15use devimint::util::{ProcessManager, poll_with_timeout};
16use devimint::version_constants::{VERSION_0_5_0_ALPHA, VERSION_0_6_0_ALPHA, VERSION_0_7_0_ALPHA};
17use devimint::{Gatewayd, LightningNode, cmd, util};
18use fedimint_core::config::FederationId;
19use fedimint_core::time::now;
20use fedimint_core::util::backoff_util::aggressive_backoff_long;
21use fedimint_core::util::retry;
22use fedimint_core::{Amount, BitcoinAmountOrAll};
23use fedimint_gateway_common::{
24    FederationInfo, GatewayBalances, GatewayFedConfig, PaymentDetails, PaymentKind, PaymentStatus,
25};
26use fedimint_logging::LOG_TEST;
27use fedimint_testing_core::node_type::LightningNodeType;
28use itertools::Itertools;
29use tracing::{debug, info, warn};
30
31#[derive(Parser)]
32struct GatewayTestOpts {
33    #[clap(subcommand)]
34    test: GatewayTest,
35}
36
37#[derive(Debug, Clone, Subcommand)]
38enum GatewayTest {
39    ConfigTest {
40        #[arg(long = "gw-type")]
41        gateway_type: LightningNodeType,
42    },
43    GatewaydMnemonic {
44        #[arg(long)]
45        old_gatewayd_path: PathBuf,
46        #[arg(long)]
47        new_gatewayd_path: PathBuf,
48        #[arg(long)]
49        old_gateway_cli_path: PathBuf,
50        #[arg(long)]
51        new_gateway_cli_path: PathBuf,
52    },
53    BackupRestoreTest,
54    LiquidityTest,
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    }
79}
80
81async fn backup_restore_test() -> anyhow::Result<()> {
82    Box::pin(
83        devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
84            let gatewayd_version = util::Gatewayd::version_or_default().await;
85            if gatewayd_version < *VERSION_0_5_0_ALPHA {
86                warn!(target: LOG_TEST, "Gateway backup-restore is not supported below v0.5.0");
87                return Ok(());
88            }
89
90            let gw = if devimint::util::supports_lnv2() {
91                dev_fed.gw_ldk_connected().await?
92            } else {
93                dev_fed.gw_lnd_registered().await?
94            };
95
96            let fed = dev_fed.fed().await?;
97            fed.pegin_gateways(10_000_000, vec![gw]).await?;
98
99            let mnemonic = gw.get_mnemonic().await?.mnemonic;
100
101            // Recover without a backup
102            info!(target: LOG_TEST, "Wiping gateway and recovering without a backup...");
103            let ln = gw.ln.clone();
104            let new_gw = stop_and_recover_gateway(
105                process_mgr.clone(),
106                mnemonic.clone(),
107                gw.to_owned(),
108                ln.clone(),
109                fed,
110            )
111            .await?;
112
113            // Recover with a backup
114            info!(target: LOG_TEST, "Wiping gateway and recovering with a backup...");
115            info!(target: LOG_TEST, "Creating backup...");
116            new_gw.backup_to_fed(fed).await?;
117            stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
118
119            info!(target: LOG_TEST, "backup_restore_test successful");
120            Ok(())
121        }),
122    )
123    .await
124}
125
126async fn stop_and_recover_gateway(
127    process_mgr: ProcessManager,
128    mnemonic: Vec<String>,
129    old_gw: Gatewayd,
130    new_ln: LightningNode,
131    fed: &Federation,
132) -> anyhow::Result<Gatewayd> {
133    let gateway_balances =
134        serde_json::from_value::<GatewayBalances>(cmd!(old_gw, "get-balances").out_json().await?)?;
135    let before_onchain_balance = gateway_balances.onchain_balance_sats;
136
137    // Stop the Gateway
138    let gw_type = old_gw.ln.ln_type();
139    let gw_name = old_gw.gw_name.clone();
140    old_gw.terminate().await?;
141    info!(target: LOG_TEST, "Terminated Gateway");
142
143    // Delete the gateway's database
144    let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
145        .expect("Data dir is not set")
146        .parse()
147        .expect("Could not parse data dir");
148    let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
149    remove_dir_all(gw_db)?;
150    info!(target: LOG_TEST, "Deleted the Gateway's database");
151
152    if gw_type == LightningNodeType::Ldk {
153        // Delete LDK's database as well
154        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    // TODO: Audit that the environment access only happens in single-threaded code.
161    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    assert_eq!(
182        10_000_000,
183        ecash_balance.ecash_balance_msats.sats_round_down()
184    );
185    let after_onchain_balance = gateway_balances.onchain_balance_sats;
186    assert_eq!(before_onchain_balance, after_onchain_balance);
187    info!(target: LOG_TEST, "Verified balances after recovery");
188
189    Ok(new_gw)
190}
191
192/// TODO(v0.5.0): We do not need to run the `gatewayd-mnemonic` test from v0.4.0
193/// -> v0.5.0 over and over again. Once we have verified this test passes for
194/// v0.5.0, it can safely be removed.
195async fn mnemonic_upgrade_test(
196    old_gatewayd_path: PathBuf,
197    new_gatewayd_path: PathBuf,
198    old_gateway_cli_path: PathBuf,
199    new_gateway_cli_path: PathBuf,
200) -> anyhow::Result<()> {
201    // TODO: Audit that the environment access only happens in single-threaded code.
202    unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
203    // TODO: Audit that the environment access only happens in single-threaded code.
204    unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
205    // TODO: Audit that the environment access only happens in single-threaded code.
206    unsafe { std::env::set_var("FM_ENABLE_MODULE_LNV2", "0") };
207
208    devimint::run_devfed_test()
209        .call(|dev_fed, process_mgr| async move {
210            let gatewayd_version = util::Gatewayd::version_or_default().await;
211            let gateway_cli_version = util::GatewayCli::version_or_default().await;
212            info!(
213                target: LOG_TEST,
214                gatewayd_version = %gatewayd_version,
215                gateway_cli_version = %gateway_cli_version,
216                "Running gatewayd mnemonic test"
217            );
218
219            let mut gw_lnd = dev_fed.gw_lnd_registered().await?.to_owned();
220            let fed = dev_fed.fed().await?;
221            let federation_id = FederationId::from_str(fed.calculate_federation_id().as_str())?;
222
223            gw_lnd
224                .restart_with_bin(&process_mgr, &new_gatewayd_path, &new_gateway_cli_path)
225                .await?;
226
227            // Gateway mnemonic is only support in >= v0.5.0
228            let new_gatewayd_version = util::Gatewayd::version_or_default().await;
229            if new_gatewayd_version < *VERSION_0_5_0_ALPHA {
230                warn!(target: LOG_TEST, "Gateway mnemonic test is not supported below v0.5.0");
231                return Ok(());
232            }
233
234            // Verify that we have a legacy federation
235            let mnemonic_response = gw_lnd.get_mnemonic().await?;
236            assert!(
237                mnemonic_response
238                    .legacy_federations
239                    .contains(&federation_id)
240            );
241
242            info!(target: LOG_TEST, "Verified a legacy federation exists");
243
244            // Leave federation
245            gw_lnd.leave_federation(federation_id).await?;
246
247            // Rejoin federation
248            gw_lnd.connect_fed(fed).await?;
249
250            // Verify that the legacy federation is recognized
251            let mnemonic_response = gw_lnd.get_mnemonic().await?;
252            assert!(
253                mnemonic_response
254                    .legacy_federations
255                    .contains(&federation_id)
256            );
257            assert_eq!(mnemonic_response.legacy_federations.len(), 1);
258
259            info!(target: LOG_TEST, "Verified leaving and re-joining preservers legacy federation");
260
261            // Leave federation and delete database to force migration to mnemonic
262            gw_lnd.leave_federation(federation_id).await?;
263
264            let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
265                .expect("Data dir is not set")
266                .parse()
267                .expect("Could not parse data dir");
268            let gw_fed_db = data_dir
269                .join(gw_lnd.gw_name.clone())
270                .join(format!("{federation_id}.db"));
271            remove_dir_all(gw_fed_db)?;
272
273            gw_lnd.connect_fed(fed).await?;
274
275            // Verify that the re-connected federation is not a legacy federation
276            let mnemonic_response = gw_lnd.get_mnemonic().await?;
277            assert!(
278                !mnemonic_response
279                    .legacy_federations
280                    .contains(&federation_id)
281            );
282            assert_eq!(mnemonic_response.legacy_federations.len(), 0);
283
284            info!(target: LOG_TEST, "Verified deleting database will migrate the federation to use mnemonic");
285
286            info!(target: LOG_TEST, "Successfully completed mnemonic upgrade test");
287
288            Ok(())
289        })
290        .await
291}
292
293/// Test that sets and verifies configurations within the gateway
294#[allow(clippy::too_many_lines)]
295async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
296    Box::pin(
297        devimint::run_devfed_test()
298            .num_feds(2)
299            .call(|dev_fed, process_mgr| async move {
300                let gatewayd_version = util::Gatewayd::version_or_default().await;
301                if gatewayd_version < *VERSION_0_5_0_ALPHA && gw_type == LightningNodeType::Ldk {
302                    return Ok(());
303                }
304
305                let gw = match gw_type {
306                    LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
307                    LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
308                };
309
310                // Try to connect to already connected federation
311                let invite_code = dev_fed.fed().await?.invite_code()?;
312                let output = cmd!(gw, "connect-fed", invite_code.clone())
313                    .out_json()
314                    .await;
315                assert!(
316                    output.is_err(),
317                    "Connecting to the same federation succeeded"
318                );
319                info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
320
321                let gatewayd_version = util::Gatewayd::version_or_default().await;
322
323                // Change the routing fees for a specific federation
324                let fed_id = dev_fed.fed().await?.calculate_federation_id();
325                gw.set_federation_routing_fee(fed_id.clone(), 20, 20000)
326                    .await?;
327
328                let lightning_fee = gw.get_lightning_fee(fed_id.clone()).await?;
329                assert_eq!(
330                    lightning_fee.base.msats, 20,
331                    "Federation base msat is not 20"
332                );
333                assert_eq!(
334                    lightning_fee.parts_per_million, 20000,
335                    "Federation proportional millionths is not 20000"
336                );
337                info!(target: LOG_TEST, "Verified per-federation routing fees changed");
338
339                let info_value = cmd!(gw, "info").out_json().await?;
340                let federations = info_value["federations"]
341                    .as_array()
342                    .expect("federations is an array");
343                assert_eq!(
344                    federations.len(),
345                    1,
346                    "Gateway did not have one connected federation"
347                );
348
349                // TODO(support:v0.4): a bug calling `gateway-cli config` was fixed in v0.5.0
350                // see: https://github.com/fedimint/fedimint/pull/5803
351                if gatewayd_version >= *VERSION_0_5_0_ALPHA
352                    && gatewayd_version < *VERSION_0_6_0_ALPHA
353                {
354                    // Get the federation's config and verify it parses correctly
355                    let config_val = cmd!(gw, "config", "--federation-id", fed_id)
356                        .out_json()
357                        .await?;
358                    serde_json::from_value::<GatewayFedConfig>(config_val)?;
359                } else if gatewayd_version >= *VERSION_0_6_0_ALPHA {
360                    // Get the federation's config and verify it parses correctly
361                    let config_val = cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
362                        .out_json()
363                        .await?;
364                    serde_json::from_value::<GatewayFedConfig>(config_val)?;
365                }
366
367                // Spawn new federation
368                let bitcoind = dev_fed.bitcoind().await?;
369                let new_fed = Federation::new(
370                    &process_mgr,
371                    bitcoind.clone(),
372                    false,
373                    false,
374                    1,
375                    "config-test".to_string(),
376                )
377                .await?;
378                let new_fed_id = new_fed.calculate_federation_id();
379                info!(target: LOG_TEST, "Successfully spawned new federation");
380
381                let new_invite_code = new_fed.invite_code()?;
382                cmd!(gw, "connect-fed", new_invite_code.clone())
383                    .out_json()
384                    .await?;
385
386                let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_6_0_ALPHA {
387                    (50000, 5000)
388                } else {
389                    (0, 10000)
390                };
391
392                let lightning_fee = gw.get_lightning_fee(new_fed_id.clone()).await?;
393                assert_eq!(
394                    lightning_fee.base.msats, default_base,
395                    "Default Base msat for new federation was not correct"
396                );
397                assert_eq!(
398                    lightning_fee.parts_per_million, default_ppm,
399                    "Default Base msat for new federation was not correct"
400                );
401
402                info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
403
404                // Peg-in sats to gw for the new fed
405                let pegin_amount = Amount::from_msats(10_000_000);
406                new_fed
407                    .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
408                    .await?;
409
410                // Verify `info` returns multiple federations
411                let info_value = cmd!(gw, "info").out_json().await?;
412                let federations = info_value["federations"]
413                    .as_array()
414                    .expect("federations is an array");
415
416                assert_eq!(
417                    federations.len(),
418                    2,
419                    "Gateway did not have two connected federations"
420                );
421
422                let federation_fake_scids =
423                    serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
424                        info_value
425                            .get("channels")
426                            .or_else(|| info_value.get("federation_fake_scids"))
427                            .expect("field  exists")
428                            .to_owned(),
429                    )
430                    .expect("cannot parse")
431                    .expect("should have scids");
432
433                assert_eq!(
434                    federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
435                    vec![1, 2]
436                );
437
438                let first_fed_info = federations
439                    .iter()
440                    .find(|i| {
441                        *i["federation_id"]
442                            .as_str()
443                            .expect("should parse as str")
444                            .to_string()
445                            == fed_id
446                    })
447                    .expect("Could not find federation");
448
449                let second_fed_info = federations
450                    .iter()
451                    .find(|i| {
452                        *i["federation_id"]
453                            .as_str()
454                            .expect("should parse as str")
455                            .to_string()
456                            == new_fed_id
457                    })
458                    .expect("Could not find federation");
459
460                let first_fed_balance_msat =
461                    serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
462                        .expect("fed should have balance");
463
464                let second_fed_balance_msat =
465                    serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
466                        .expect("fed should have balance");
467
468                assert_eq!(first_fed_balance_msat, Amount::ZERO);
469                assert_eq!(second_fed_balance_msat, pegin_amount);
470
471                leave_federation(gw, fed_id, 1).await?;
472                leave_federation(gw, new_fed_id, 2).await?;
473
474                // Rejoin new federation, verify that the balance is the same
475                let output = cmd!(gw, "connect-fed", new_invite_code.clone())
476                    .out_json()
477                    .await?;
478                let rejoined_federation_balance_msat =
479                    serde_json::from_value::<Amount>(output["balance_msat"].clone())
480                        .expect("fed has balance");
481
482                assert_eq!(second_fed_balance_msat, rejoined_federation_balance_msat);
483
484                info!(target: LOG_TEST, "Gateway configuration test successful");
485                Ok(())
486            }),
487    )
488    .await
489}
490
491/// Test that verifies the various liquidity tools (onchain, lightning, ecash)
492/// work correctly.
493#[allow(clippy::too_many_lines)]
494async fn liquidity_test() -> anyhow::Result<()> {
495    devimint::run_devfed_test()
496        .call(|dev_fed, _process_mgr| async move {
497            let federation = dev_fed.fed().await?;
498
499            if !devimint::util::supports_lnv2() {
500                info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
501                return Ok(());
502            }
503
504            let gw_lnd = dev_fed.gw_lnd_registered().await?;
505            let gw_ldk = dev_fed.gw_ldk_connected().await?;
506            let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
507            let gateways = [gw_lnd, gw_ldk].to_vec();
508
509            let gateway_matrix = gateways
510                .iter()
511                .cartesian_product(gateways.iter())
512                .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
513
514            info!(target: LOG_TEST, "Pegging-in gateways...");
515            federation
516                .pegin_gateways(1_000_000, gateways.clone())
517                .await?;
518
519            info!(target: LOG_TEST, "Testing ecash payments between gateways...");
520            for (gw_send, gw_receive) in gateway_matrix.clone() {
521                info!(
522                    target: LOG_TEST,
523                    gw_send = %gw_send.ln.ln_type(),
524                    gw_receive = %gw_receive.ln.ln_type(),
525                    "Testing ecash payment",
526                );
527
528                let fed_id = federation.calculate_federation_id();
529                let prev_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
530                let prev_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
531                let ecash = gw_send.send_ecash(fed_id.clone(), 500_000).await?;
532                gw_receive.receive_ecash(ecash).await?;
533                let after_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
534                let after_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
535                assert_eq!(prev_send_ecash_balance - 500_000, after_send_ecash_balance);
536                assert_eq!(
537                    prev_receive_ecash_balance + 500_000,
538                    after_receive_ecash_balance
539                );
540            }
541
542            info!(target: LOG_TEST, "Testing payments between gateways...");
543            for (gw_send, gw_receive) in gateway_matrix.clone() {
544                info!(
545                    target: LOG_TEST,
546                    gw_send = %gw_send.ln.ln_type(),
547                    gw_receive = %gw_receive.ln.ln_type(),
548                    "Testing lightning payment",
549                );
550
551                let invoice = gw_receive.create_invoice(1_000_000).await?;
552                gw_send.pay_invoice(invoice).await?;
553            }
554
555            if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
556                let start = now() - Duration::from_secs(5 * 60);
557                let end = now() + Duration::from_secs(5 * 60);
558                info!(target: LOG_TEST, "Verifying list of transactions");
559                let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
560                // One inbound and one outbound transaction
561                assert_eq!(lnd_transactions.len(), 2);
562
563                let ldk_transactions = gw_ldk.list_transactions(start, end).await?;
564                assert_eq!(ldk_transactions.len(), 2);
565
566                // Verify that transactions are filtered by time
567                let start = now() - Duration::from_secs(10 * 60);
568                let end = now() - Duration::from_secs(5 * 60);
569                let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
570                assert_eq!(lnd_transactions.len(), 0);
571            }
572
573            info!(target: LOG_TEST, "Testing paying through LND Gateway...");
574            // Need to try to pay the invoice multiple times in case the channel graph has
575            // not been updated yet. LDK's channel graph updates slowly in these
576            // tests, so we need to try for awhile.
577            poll_with_timeout("LDK2 pay LDK", Duration::from_secs(180), || async {
578                debug!(target: LOG_TEST, "Trying LDK2 -> LND -> LDK...");
579                let invoice = gw_ldk
580                    .create_invoice(1_550_000)
581                    .await
582                    .map_err(ControlFlow::Continue)?;
583                gw_ldk_second
584                    .pay_invoice(invoice.clone())
585                    .await
586                    .map_err(ControlFlow::Continue)?;
587                Ok(())
588            })
589            .await?;
590
591            if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
592                info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
593                // TODO: investigate why the first BOLT12 payment attempt is expiring consistently
594                poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
595                    let offer_with_amount = gw_ldk_second.create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
596                    gw_ldk.pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
597                    assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
598                    Ok(())
599                }).await?;
600
601                let offer_without_amount = gw_ldk.create_offer(None).await?;
602                gw_ldk_second.pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
603                assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
604
605                // Cannot pay an offer without an amount without specifying an amount
606                gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
607
608                // Verify we can pay the offer again
609                gw_ldk_second.pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
610                assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
611            }
612
613            info!(target: LOG_TEST, "Pegging-out gateways...");
614            federation
615                .pegout_gateways(500_000_000, gateways.clone())
616                .await?;
617
618            info!(target: LOG_TEST, "Testing closing all channels...");
619            gw_ldk_second.close_all_channels().await?;
620            gw_ldk.close_all_channels().await?;
621            let bitcoind = dev_fed.bitcoind().await?;
622            // Need to mine enough blocks in case the channels are force closed.
623            bitcoind.mine_blocks(2016).await?;
624            let block_height = bitcoind.get_block_count().await? - 1;
625            gw_ldk_second.wait_for_block_height(block_height).await?;
626            check_empty_lightning_balance(gw_ldk_second).await?;
627            gw_ldk.wait_for_block_height(block_height).await?;
628            check_empty_lightning_balance(gw_ldk).await?;
629            check_empty_lightning_balance(gw_lnd).await?;
630
631            info!(target: LOG_TEST, "Testing sending onchain...");
632            // TODO: LND is sometimes keeping 10000 sats as reserve when sending onchain
633            // For now we will skip checking LND.
634            gw_ldk
635                .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
636                .await?;
637            gw_ldk_second
638                .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
639                .await?;
640            check_empty_onchain_balance(gw_ldk).await?;
641            check_empty_onchain_balance(gw_ldk_second).await?;
642
643            Ok(())
644        })
645        .await
646}
647
648async fn get_transaction(
649    gateway: &Gatewayd,
650    kind: PaymentKind,
651    amount: Amount,
652    status: PaymentStatus,
653) -> Option<PaymentDetails> {
654    let transactions = gateway
655        .list_transactions(
656            now() - Duration::from_secs(5 * 60),
657            now() + Duration::from_secs(5 * 60),
658        )
659        .await
660        .ok()?;
661    transactions.into_iter().find(|details| {
662        details.payment_kind == kind && details.amount == amount && details.status == status
663    })
664}
665
666async fn check_empty_onchain_balance(gw: &Gatewayd) -> anyhow::Result<()> {
667    retry(
668        "Wait for onchain balance update",
669        aggressive_backoff_long(),
670        || async {
671            let curr_balance = gw.get_balances().await?.onchain_balance_sats;
672            let gw_name = gw.gw_name.clone();
673            ensure!(
674                curr_balance == 0,
675                "Gateway onchain balance is not empty: {curr_balance} gw_name: {gw_name}"
676            );
677            Ok(())
678        },
679    )
680    .await
681}
682
683async fn check_empty_lightning_balance(gw: &Gatewayd) -> anyhow::Result<()> {
684    let balances = gw.get_balances().await?;
685    let curr_lightning_balance = balances.lightning_balance_msats;
686    ensure!(
687        curr_lightning_balance == 0,
688        "Close channels did not sweep all lightning funds"
689    );
690    let inbound_lightning_balance = balances.inbound_lightning_liquidity_msats;
691    ensure!(
692        inbound_lightning_balance == 0,
693        "Close channels did not sweep all lightning funds"
694    );
695    Ok(())
696}
697
698/// Leaves the specified federation by issuing a `leave-fed` POST request to the
699/// gateway.
700async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
701    let gatewayd_version = util::Gatewayd::version_or_default().await;
702    let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
703        .out_json()
704        .await
705        .expect("Leaving the federation failed");
706
707    let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
708    assert_eq!(federation_id.to_string(), fed_id);
709
710    // TODO(support:v0.4): `federation_index` was introduced in v0.5.0
711    // see: https://github.com/fedimint/fedimint/pull/5971
712    let scid = if gatewayd_version < *VERSION_0_5_0_ALPHA {
713        let channel_id: Option<u64> = serde_json::from_value(leave_fed["channel_id"].clone())?;
714        channel_id.expect("must have channel id")
715    } else if gatewayd_version >= *VERSION_0_5_0_ALPHA && gatewayd_version < *VERSION_0_6_0_ALPHA {
716        serde_json::from_value::<u64>(leave_fed["federation_index"].clone())?
717    } else {
718        serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?
719    };
720
721    assert_eq!(scid, expected_scid);
722
723    info!(target: LOG_TEST, federation_id = %fed_id, "Verified gateway left federation");
724    Ok(())
725}