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    let old_gw_index = old_gw.gateway_index;
136    old_gw.terminate().await?;
137    info!(target: LOG_TEST, "Terminated Gateway");
138
139    // Delete the gateway's database
140    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        // db is single file on redb
147        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        // Delete LDK's database as well
155        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    // TODO: Audit that the environment access only happens in single-threaded code.
162    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
195/// TODO(v0.5.0): We do not need to run the `gatewayd-mnemonic` test from v0.4.0
196/// -> v0.5.0 over and over again. Once we have verified this test passes for
197/// v0.5.0, it can safely be removed.
198async 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    // TODO: Audit that the environment access only happens in single-threaded code.
205    unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
206    // TODO: Audit that the environment access only happens in single-threaded code.
207    unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
208    // TODO: Audit that the environment access only happens in single-threaded code.
209    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            // Verify that we have a legacy federation
231            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            // Leave federation
241            gw_lnd.leave_federation(federation_id).await?;
242
243            // Rejoin federation
244            gw_lnd.connect_fed(fed).await?;
245
246            // Verify that the legacy federation is recognized
247            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            // Leave federation and delete database to force migration to mnemonic
258            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            // Verify that the re-connected federation is not a legacy federation
272            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/// Test that sets and verifies configurations within the gateway
290#[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                // Try to connect to already connected federation
302                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                // Change the routing fees for a specific federation
315                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                // Get the federation's config and verify it parses correctly
341                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                // Spawn new federation
348                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                // Peg-in sats to gw for the new fed
386                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                // Verify `info` returns multiple federations
392                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                // Rejoin new federation, verify that the balance is the same
456                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                    // Try to get the info over iroh
467                    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/// Test that verifies the various liquidity tools (onchain, lightning, ecash)
479/// work correctly.
480#[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            // One inbound and one outbound transaction
549            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            // Verify that transactions are filtered by time
555            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            // TODO: investigate why the first BOLT12 payment attempt is expiring consistently
562            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            // Cannot pay an offer without an amount without specifying an amount
574            gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
575
576            // Verify we can pay the offer again
577            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            // Gracefully close one of LND's channel's
597            let gw_ldk_pubkey = gw_ldk.lightning_pubkey().await?;
598            gw_lnd.close_channel(gw_ldk_pubkey, false).await?;
599
600            // Force close LDK's channels
601            gw_ldk_second.close_all_channels(true).await?;
602
603            // Verify none of the channels are active
604            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
687/// Leaves the specified federation by issuing a `leave-fed` POST request to the
688/// gateway.
689async 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}