gateway_tests/
main.rs

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            // Recover without a backup
97            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            // Recover with a backup
109            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    // Stop the Gateway
133    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    // Delete the gateway's database
139    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        // db is single file on redb
146        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        // 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    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
194/// TODO(v0.5.0): We do not need to run the `gatewayd-mnemonic` test from v0.4.0
195/// -> v0.5.0 over and over again. Once we have verified this test passes for
196/// v0.5.0, it can safely be removed.
197async 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    // TODO: Audit that the environment access only happens in single-threaded code.
204    unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
205    // TODO: Audit that the environment access only happens in single-threaded code.
206    unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
207    // TODO: Audit that the environment access only happens in single-threaded code.
208    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            // Verify that we have a legacy federation
230            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            // Leave federation
240            gw_lnd.leave_federation(federation_id).await?;
241
242            // Rejoin federation
243            gw_lnd.connect_fed(fed).await?;
244
245            // Verify that the legacy federation is recognized
246            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            // Leave federation and delete database to force migration to mnemonic
257            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            // Verify that the re-connected federation is not a legacy federation
271            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/// Test that sets and verifies configurations within the gateway
289#[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                // Try to connect to already connected federation
301                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                // Change the routing fees for a specific federation
314                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                // Get the federation's config and verify it parses correctly
340                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                // Spawn new federation
347                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                // Peg-in sats to gw for the new fed
385                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                // Verify `info` returns multiple federations
391                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                // Rejoin new federation, verify that the balance is the same
455                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                    // Try to get the info over iroh
466                    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/// Test that verifies the various liquidity tools (onchain, lightning, ecash)
478/// work correctly.
479#[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            // One inbound and one outbound transaction
548            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            // Verify that transactions are filtered by time
554            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            // TODO: investigate why the first BOLT12 payment attempt is expiring consistently
561            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            // Cannot pay an offer without an amount without specifying an amount
573            gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
574
575            // Verify we can pay the offer again
576            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            // Gracefully close one of LND's channel's
596            let gw_ldk_pubkey = gw_ldk.lightning_pubkey().await?;
597            gw_lnd.close_channel(gw_ldk_pubkey, false).await?;
598
599            // Force close LDK's channels
600            gw_ldk_second.close_all_channels(true).await?;
601
602            // Verify none of the channels are active
603            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
686/// Leaves the specified federation by issuing a `leave-fed` POST request to the
687/// gateway.
688async 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}