Skip to main content

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 anyhow::Context;
12use clap::{Parser, Subcommand};
13use devimint::cli::cleanup_on_exit;
14use devimint::envs::FM_DATA_DIR_ENV;
15use devimint::external::{Bitcoind, Esplora};
16use devimint::federation::Federation;
17use devimint::util::{ProcessManager, almost_equal, poll, poll_with_timeout};
18use devimint::version_constants::{
19    VERSION_0_10_0_ALPHA, VERSION_0_12_0_ALPHA, VERSION_0_13_0_ALPHA,
20};
21use devimint::{Gatewayd, LightningNode, cli, util};
22use fedimint_core::config::FederationId;
23use fedimint_core::time::now;
24use fedimint_core::{Amount, BitcoinAmountOrAll, bitcoin, default_esplora_server};
25use fedimint_gateway_common::{FederationInfo, PaymentDetails, PaymentKind, PaymentStatus};
26use fedimint_logging::LOG_TEST;
27use fedimint_testing_core::node_type::LightningNodeType;
28use itertools::Itertools;
29use tracing::info;
30
31#[derive(Parser)]
32struct GatewayTestOpts {
33    #[clap(subcommand)]
34    test: GatewayTest,
35}
36
37#[derive(Debug, Clone, Subcommand)]
38#[allow(clippy::enum_variant_names)]
39enum GatewayTest {
40    ConfigTest {
41        #[arg(long = "gw-type")]
42        gateway_type: LightningNodeType,
43    },
44    BackupRestoreTest,
45    LiquidityTest,
46    EsploraTest,
47}
48
49#[tokio::main]
50async fn main() -> anyhow::Result<()> {
51    let opts = GatewayTestOpts::parse();
52    match opts.test {
53        GatewayTest::ConfigTest { gateway_type } => Box::pin(config_test(gateway_type)).await,
54        GatewayTest::BackupRestoreTest => Box::pin(backup_restore_test()).await,
55        GatewayTest::LiquidityTest => Box::pin(liquidity_test()).await,
56        GatewayTest::EsploraTest => esplora_test().await,
57    }
58}
59
60async fn backup_restore_test() -> anyhow::Result<()> {
61    Box::pin(
62        devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
63            let gw = if devimint::util::supports_lnv2() {
64                dev_fed.gw_ldk_connected().await?
65            } else {
66                dev_fed.gw_lnd_registered().await?
67            };
68
69            let fed = dev_fed.fed().await?;
70            fed.pegin_gateways(10_000_000, vec![gw]).await?;
71
72            let mnemonic = gw.client().get_mnemonic().await?.mnemonic;
73
74            // Recover without a backup
75            info!(target: LOG_TEST, "Wiping gateway and recovering without a backup...");
76            let ln = gw.ln.clone();
77            let new_gw = stop_and_recover_gateway(
78                process_mgr.clone(),
79                mnemonic.clone(),
80                gw.to_owned(),
81                ln.clone(),
82                fed,
83            )
84            .await?;
85
86            // Recover with a backup
87            info!(target: LOG_TEST, "Wiping gateway and recovering with a backup...");
88            info!(target: LOG_TEST, "Creating backup...");
89            new_gw.client().backup_to_fed(fed).await?;
90            stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
91
92            info!(target: LOG_TEST, "backup_restore_test successful");
93            Ok(())
94        }),
95    )
96    .await
97}
98
99async fn stop_and_recover_gateway(
100    process_mgr: ProcessManager,
101    mnemonic: Vec<String>,
102    old_gw: Gatewayd,
103    new_ln: LightningNode,
104    fed: &Federation,
105) -> anyhow::Result<Gatewayd> {
106    let gateway_balances = old_gw.client().get_balances().await?;
107    let before_onchain_balance = gateway_balances.onchain_balance_sats;
108
109    // Stop the Gateway
110    let gw_type = old_gw.ln.ln_type();
111    let gw_name = old_gw.gw_name.clone();
112    let old_gw_index = old_gw.gateway_index;
113    old_gw.terminate().await?;
114    info!(target: LOG_TEST, "Terminated Gateway");
115
116    // Delete the gateway's database
117    let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
118        .expect("Data dir is not set")
119        .parse()
120        .expect("Could not parse data dir");
121    let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
122    if gw_db.is_file() {
123        // db is single file on redb
124        remove_file(gw_db)?;
125    } else {
126        remove_dir_all(gw_db)?;
127    }
128    info!(target: LOG_TEST, "Deleted the Gateway's database");
129
130    if gw_type == LightningNodeType::Ldk {
131        // Delete LDK's database as well
132        let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
133        remove_dir_all(ldk_data_dir)?;
134        info!(target: LOG_TEST, "Deleted LDK's database");
135    }
136
137    let seed = mnemonic.join(" ");
138    // TODO: Audit that the environment access only happens in single-threaded code.
139    unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
140    let new_gw = Gatewayd::new(&process_mgr, new_ln, old_gw_index).await?;
141    let new_mnemonic = new_gw.client().get_mnemonic().await?.mnemonic;
142    assert_eq!(mnemonic, new_mnemonic);
143    info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
144
145    let federations = serde_json::from_value::<Vec<FederationInfo>>(
146        new_gw.client().get_info().await?["federations"].clone(),
147    )?;
148    assert_eq!(0, federations.len());
149    info!(target: LOG_TEST, "Verified new Gateway has no federations");
150
151    new_gw.client().recover_fed(fed).await?;
152
153    let gateway_balances = new_gw.client().get_balances().await?;
154    let ecash_balance = gateway_balances
155        .ecash_balances
156        .first()
157        .expect("Should have one joined federation");
158    almost_equal(
159        ecash_balance.ecash_balance_msats.sats_round_down(),
160        10_000_000,
161        10,
162    )
163    .unwrap();
164    let after_onchain_balance = gateway_balances.onchain_balance_sats;
165    assert_eq!(before_onchain_balance, after_onchain_balance);
166    info!(target: LOG_TEST, "Verified balances after recovery");
167
168    Ok(new_gw)
169}
170
171/// Test that sets and verifies configurations within the gateway
172#[allow(clippy::too_many_lines)]
173async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
174    Box::pin(
175        devimint::run_devfed_test()
176            .num_feds(2)
177            .call(|dev_fed, process_mgr| async move {
178                let gw = match gw_type {
179                    LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
180                    LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
181                };
182
183                // Try to connect to already connected federation
184                let invite_code = dev_fed.fed().await?.invite_code()?;
185                gw.client().connect_fed(invite_code).await.expect_err("Connecting to the same federation succeeded");
186                info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
187
188                // Change the routing fees for a specific federation
189                let fed_id = dev_fed.fed().await?.calculate_federation_id();
190                // The gateway rejects a lightning fee whose sum with the transaction fee
191                // exceeds `PaymentFee::SEND_FEE_LIMIT` in either component. The default
192                // transaction fee contributes 3_000 parts per million, so the lightning
193                // fee has to stay at or below 12_000 to keep the total within the 15_000
194                // limit.
195                gw.client().set_federation_routing_fee(fed_id.clone(), 20, 10000)
196                    .await?;
197
198                let lightning_fee = gw.client().get_lightning_fee(fed_id.clone()).await?;
199                assert_eq!(
200                    lightning_fee.base.msats, 20,
201                    "Federation base msat is not 20"
202                );
203                assert_eq!(
204                    lightning_fee.parts_per_million, 10000,
205                    "Federation proportional millionths is not 10000"
206                );
207                info!(target: LOG_TEST, "Verified per-federation routing fees changed");
208
209                let info_value = gw.client().get_info().await?;
210                let federations = info_value["federations"]
211                    .as_array()
212                    .expect("federations is an array");
213                assert_eq!(
214                    federations.len(),
215                    1,
216                    "Gateway did not have one connected federation"
217                );
218
219                // Get the federation's config and verify it parses correctly
220                gw.client().client_config(fed_id.clone()).await?;
221
222                // Spawn new federation
223                let bitcoind = dev_fed.bitcoind().await?;
224                let new_fed = Federation::new(
225                    &process_mgr,
226                    bitcoind.clone(),
227                    false,
228                    false,
229                    false,
230                    1,
231                    "config-test".to_string(),
232                )
233                .await?;
234                let new_fed_id = new_fed.calculate_federation_id();
235                info!(target: LOG_TEST, "Successfully spawned new federation");
236
237                let new_invite_code = new_fed.invite_code()?;
238                gw.client().connect_fed(new_invite_code.clone()).await?;
239
240                let default_base = 0;
241                let default_ppm = 0;
242
243                let lightning_fee = gw.client().get_lightning_fee(new_fed_id.clone()).await?;
244                assert_eq!(
245                    lightning_fee.base.msats, default_base,
246                    "Default Base msat for new federation was not correct"
247                );
248                assert_eq!(
249                    lightning_fee.parts_per_million, default_ppm,
250                    "Default Base msat for new federation was not correct"
251                );
252
253                info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
254
255                // Peg-in sats to gw for the new fed
256                let pegin_amount = Amount::from_msats(10_000_000);
257                new_fed
258                    .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
259                    .await?;
260
261                // Verify `info` returns multiple federations
262                let info_value = gw.client().get_info().await?;
263                let federations = info_value["federations"]
264                    .as_array()
265                    .expect("federations is an array");
266
267                assert_eq!(
268                    federations.len(),
269                    2,
270                    "Gateway did not have two connected federations"
271                );
272
273                let federation_fake_scids =
274                    serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
275                        info_value
276                            .get("channels")
277                            .or_else(|| info_value.get("federation_fake_scids"))
278                            .expect("field  exists")
279                            .to_owned(),
280                    )
281                    .expect("cannot parse")
282                    .expect("should have scids");
283
284                assert_eq!(
285                    federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
286                    vec![1, 2]
287                );
288
289                let first_fed_info = federations
290                    .iter()
291                    .find(|i| {
292                        *i["federation_id"]
293                            .as_str()
294                            .expect("should parse as str")
295                            .to_string()
296                            == fed_id
297                    })
298                    .expect("Could not find federation");
299
300                let second_fed_info = federations
301                    .iter()
302                    .find(|i| {
303                        *i["federation_id"]
304                            .as_str()
305                            .expect("should parse as str")
306                            .to_string()
307                            == new_fed_id
308                    })
309                    .expect("Could not find federation");
310
311                let first_fed_balance_msat =
312                    serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
313                        .expect("fed should have balance");
314
315                let second_fed_balance_msat =
316                    serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
317                        .expect("fed should have balance");
318
319                assert_eq!(first_fed_balance_msat, Amount::ZERO);
320                almost_equal(second_fed_balance_msat.msats, pegin_amount.msats, 10_000).unwrap();
321
322                let fed_id = FederationId::from_str(&fed_id).expect("invalid Federation ID");
323                let fed_info = gw.client().leave_federation(fed_id).await?;
324                assert_eq!(serde_json::from_value::<FederationId>(fed_info["federation_id"].clone())?, fed_id);
325                assert_eq!(fed_info["config"]["federation_index"].as_u64().expect("Was not u64"), 1);
326                gw.client().leave_federation(fed_id).await.expect_err("Successfully left a federation twice");
327
328                let new_fed_id = FederationId::from_str(&new_fed_id).expect("invalid Federation ID");
329                let fed_info = gw.client().leave_federation(new_fed_id).await?;
330                assert_eq!(serde_json::from_value::<FederationId>(fed_info["federation_id"].clone())?, new_fed_id);
331                assert_eq!(fed_info["config"]["federation_index"].as_u64().expect("Was not u64"), 2);
332
333                // Rejoin new federation, verify that the balance is the same
334                let fed_info = gw.client().connect_fed(new_invite_code).await?;
335                assert_eq!(second_fed_balance_msat, Amount::from_msats(fed_info["balance_msat"].as_u64().expect("Balance should be present")));
336
337                if gw.gatewayd_version >= *VERSION_0_10_0_ALPHA {
338                    // Try to get the info over iroh
339                    info!(target: LOG_TEST, gatewayd_version = %gw.gatewayd_version, "Getting info over iroh");
340                    gw.client().with_iroh().get_info().await?;
341                }
342
343                info!(target: LOG_TEST, "Gateway configuration test successful");
344                Ok(())
345            }),
346    )
347    .await
348}
349
350/// Test that verifies the various liquidity tools (onchain, lightning, ecash)
351/// work correctly.
352#[allow(clippy::too_many_lines)]
353async fn liquidity_test() -> anyhow::Result<()> {
354    devimint::run_devfed_test()
355        .call(|dev_fed, _process_mgr| async move {
356            let federation = dev_fed.fed().await?;
357
358            if !devimint::util::supports_lnv2() {
359                info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
360                return Ok(());
361            }
362
363            let gw_lnd = dev_fed.gw_lnd_registered().await?;
364            let gw_ldk = dev_fed.gw_ldk_connected().await?;
365            let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
366            let gateways = [gw_lnd, gw_ldk].to_vec();
367
368            let gateway_matrix = gateways
369                .iter()
370                .cartesian_product(gateways.iter())
371                .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
372
373            info!(target: LOG_TEST, "Pegging-in gateways...");
374            federation
375                .pegin_gateways(1_000_000, gateways.clone())
376                .await?;
377
378            info!(target: LOG_TEST, "Testing ecash payments between gateways...");
379            for (gw_send, gw_receive) in gateway_matrix.clone() {
380                info!(
381                    target: LOG_TEST,
382                    gw_send = %gw_send.ln.ln_type(),
383                    gw_receive = %gw_receive.ln.ln_type(),
384                    "Testing ecash payment",
385                );
386
387                let fed_id = federation.calculate_federation_id();
388                let prev_send_ecash_balance = gw_send.client().ecash_balance(fed_id.clone()).await?;
389                let prev_receive_ecash_balance = gw_receive.client().ecash_balance(fed_id.clone()).await?;
390                let ecash = gw_send.client().send_ecash(fed_id.clone(), 500_000).await?;
391                gw_receive.client().receive_ecash(ecash).await?;
392                let after_send_ecash_balance = gw_send.client().ecash_balance(fed_id.clone()).await?;
393                almost_equal(
394                    prev_send_ecash_balance - 500_000,
395                    after_send_ecash_balance,
396                    if util::supports_mint_v2() { 2_000 } else { 512 },
397                )
398                .expect("Balances were not almost equal");
399
400                poll_with_timeout(
401                    "receive ecash balance",
402                    Duration::from_secs(30),
403                    || async {
404                        let balance = gw_receive.client().ecash_balance(fed_id.clone()).await
405                            .map_err(ControlFlow::Break)?;
406                        almost_equal(prev_receive_ecash_balance + 500_000, balance, 2_000)
407                            .map_err(|e| ControlFlow::Continue(anyhow::anyhow!(e)))
408                    },
409                )
410                .await?;
411            }
412
413            info!(target: LOG_TEST, "Testing payments between gateways...");
414            for (gw_send, gw_receive) in gateway_matrix.clone() {
415                info!(
416                    target: LOG_TEST,
417                    gw_send = %gw_send.ln.ln_type(),
418                    gw_receive = %gw_receive.ln.ln_type(),
419                    "Testing lightning payment",
420                );
421
422                let invoice = gw_receive.client().create_invoice(1_000_000).await?;
423                gw_send.client().pay_invoice(invoice).await?;
424            }
425
426            let start = now() - Duration::from_mins(5);
427            let end = now() + Duration::from_mins(5);
428            info!(target: LOG_TEST, "Verifying list of transactions");
429            let lnd_transactions = gw_lnd.client().list_transactions(start, end).await?;
430            // One inbound and one outbound transaction
431            assert_eq!(lnd_transactions.len(), 2);
432
433            let ldk_transactions = gw_ldk.client().list_transactions(start, end).await?;
434            assert_eq!(ldk_transactions.len(), 2);
435
436            // Verify that transactions are filtered by time
437            let start = now() - Duration::from_mins(10);
438            let end = now() - Duration::from_mins(5);
439            let lnd_transactions = gw_lnd.client().list_transactions(start, end).await?;
440            assert_eq!(lnd_transactions.len(), 0);
441
442            info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
443            let offer_with_amount = gw_ldk_second.client().create_offer(Some(Amount::from_msats(10_000_000))).await?;
444            gw_ldk.client().pay_offer(offer_with_amount, None).await?;
445            assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
446
447            let offer_without_amount = gw_ldk.client().create_offer(None).await?;
448            gw_ldk_second.client().pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
449            assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
450
451            // Cannot pay an offer without an amount without specifying an amount
452            gw_ldk_second.client().pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
453
454            // Verify we can pay the offer again
455            gw_ldk_second.client().pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
456            assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
457
458            // `set-channel-fees` was added in 0.12.0-alpha for both the gateway
459            // API and the CLI. Skip the test against any older gateway/CLI binary
460            // to keep this test backwards-compatible with prior releases.
461            let gateway_cli_version = util::GatewayCli::version_or_default().await;
462            let all_gateways_support_fees = gateways
463                .iter()
464                .all(|gw| gw.gatewayd_version >= *VERSION_0_12_0_ALPHA);
465            if gateway_cli_version >= *VERSION_0_12_0_ALPHA && all_gateways_support_fees {
466                info!(target: LOG_TEST, "Testing updating channel fees on both gateways...");
467                for gw in &gateways {
468                    let channels = gw.client().list_channels().await?;
469                    let channel = channels
470                        .into_iter()
471                        .find(|c| c.funding_outpoint.is_some())
472                        .with_context(|| {
473                            format!(
474                                "{} gateway has no channel with a known funding outpoint",
475                                gw.ln.ln_type(),
476                            )
477                        })?;
478                    let funding_outpoint = channel.funding_outpoint.expect("filtered above");
479
480                    // Pick values that are unlikely to collide with any backend default.
481                    let new_base_fee_msat = 12_345u64;
482                    let new_parts_per_million = 678u64;
483
484                    gw.client()
485                        .set_channel_fees(
486                            funding_outpoint,
487                            new_base_fee_msat,
488                            new_parts_per_million,
489                        )
490                        .await?;
491
492                    // Both backends report local config synchronously, but poll briefly
493                    // in case the LND policy update needs a moment to be visible to
494                    // `fee_report`.
495                    poll_with_timeout(
496                        "channel fees reflect updated values",
497                        Duration::from_secs(15),
498                        || async {
499                            let updated = gw
500                                .client()
501                                .list_channels()
502                                .await
503                                .map_err(ControlFlow::Continue)?
504                                .into_iter()
505                                .find(|c| c.funding_outpoint == Some(funding_outpoint))
506                                .ok_or_else(|| {
507                                    ControlFlow::Break(anyhow::anyhow!(
508                                        "channel disappeared after fee update"
509                                    ))
510                                })?;
511                            if updated.base_fee_msat == Some(new_base_fee_msat)
512                                && updated.parts_per_million == Some(new_parts_per_million)
513                            {
514                                Ok(())
515                            } else {
516                                Err(ControlFlow::Continue(anyhow::anyhow!(
517                                    "{} gateway still reports base={:?}, ppm={:?}",
518                                    gw.ln.ln_type(),
519                                    updated.base_fee_msat,
520                                    updated.parts_per_million,
521                                )))
522                            }
523                        },
524                    )
525                    .await?;
526                }
527            } else {
528                info!(
529                    target: LOG_TEST,
530                    gateway_cli_version = %gateway_cli_version,
531                    "Skipping set-channel-fees test (requires gateway >= 0.12.0-alpha)"
532                );
533            }
534
535            info!(target: LOG_TEST, "Pegging-out gateways...");
536            federation
537                .pegout_gateways(500_000_000, gateways.clone())
538                .await?;
539
540            let bitcoind = dev_fed.bitcoind().await?;
541            let supports_fee_aware_sweep = *VERSION_0_13_0_ALPHA <= gw_lnd.gatewayd_version;
542            if supports_fee_aware_sweep {
543                info!(target: LOG_TEST, "Testing pegging-out the entire ecash balance...");
544                // Sweeping the full balance has to leave room for the federation's
545                // per-note fees on top of the on-chain fee. With base fees enabled
546                // (the default) a naive `balance - onchain_fee` is underfunded and
547                // note selection rejects the peg-out. Only one gateway is drained:
548                // the required feerate doubles per pending federation transaction,
549                // and the federation needs a non-dust change UTXO of its own.
550                let fed_id = federation.calculate_federation_id();
551                let sweep_balance = gw_lnd.client().ecash_balance(fed_id.clone()).await?;
552                assert!(sweep_balance > 0, "Gateway has no ecash left to sweep");
553
554                let pegout_address = bitcoind.get_new_address().await?;
555                let sweep = gw_lnd
556                    .client()
557                    .pegout_all(fed_id.clone(), pegout_address)
558                    .await?;
559                bitcoind.mine_blocks(21).await?;
560                bitcoind.poll_get_transaction(sweep.txid).await?;
561
562                // A sweep cannot always drain to exactly zero — fees are stepwise in
563                // the amount, so sub-denomination dust can be left behind — but it
564                // must move all but a negligible remainder.
565                let remaining = gw_lnd.client().ecash_balance(fed_id).await?;
566                assert!(
567                    remaining < sweep_balance / 100,
568                    "Sweep left {remaining} msats of {sweep_balance} msats behind",
569                );
570            } else {
571                // Older gateways subtract only the on-chain fee from `--amount all`.
572                // They cannot reserve the current federation's per-note fees, so the
573                // sweep is deterministically rejected. Keep the test for gateways
574                // that implement fee-aware sweeps while the back-compat matrix tests
575                // the remaining liquidity operations against historical binaries.
576                info!(
577                    target: LOG_TEST,
578                    gatewayd_version = %gw_lnd.gatewayd_version,
579                    minimum_gatewayd_version = %*VERSION_0_13_0_ALPHA,
580                    "Skipping full ecash sweep (requires fee-aware gateway)"
581                );
582            }
583
584            info!(target: LOG_TEST, "Testing only admin can send onchain...");
585            let send_result = gw_lnd.client().with_password("secondbest").send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10).await;
586            assert!(send_result.is_err(), "Only admins can send onchain");
587
588            info!(target: LOG_TEST, "Testing sending onchain...");
589            for gw in &gateways {
590                let txid = gw
591                    .client()
592                    .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
593                    .await?;
594                bitcoind.poll_get_transaction(txid).await?;
595            }
596
597            info!(target: LOG_TEST, "Testing closing all channels...");
598
599            // Gracefully close one of LND's channel's
600            let gw_ldk_pubkey = gw_ldk.client().lightning_pubkey().await?;
601            gw_lnd.client().close_channel(gw_ldk_pubkey, false).await?;
602
603            // Force close remaining channels on every gateway and wait
604            for gw in &gateways {
605                gw.client()
606                    .close_all_channels(true, Duration::from_secs(30))
607                    .await?;
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                    metrics_port: process_mgr.globals.FM_PORT_GW_LDK_METRICS,
640                },
641                0,
642            )
643            .await?;
644
645            info!("Waiting for ldk gatewy to be ready...");
646            poll("Waiting for LDK to be ready", || async {
647                let info = ldk
648                    .client()
649                    .get_info()
650                    .await
651                    .map_err(ControlFlow::Continue)?;
652                let state: String = serde_json::from_value(info["gateway_state"].clone())
653                    .expect("Could not get gateway state");
654                if state == "Running" {
655                    Ok(())
656                } else {
657                    Err(ControlFlow::Continue(anyhow::anyhow!(
658                        "Gateway not running"
659                    )))
660                }
661            })
662            .await?;
663
664            ldk.client().get_ln_onchain_address().await?;
665            info!(target:LOG_TEST, "ldk gateway successfully spawned and connected to esplora");
666            Ok(())
667        },
668        task_group,
669    )
670    .await?;
671    Ok(())
672}
673
674async fn get_transaction(
675    gateway: &Gatewayd,
676    kind: PaymentKind,
677    amount: Amount,
678    status: PaymentStatus,
679) -> Option<PaymentDetails> {
680    let transactions = gateway
681        .client()
682        .list_transactions(
683            now() - Duration::from_mins(5),
684            now() + Duration::from_mins(5),
685        )
686        .await
687        .ok()?;
688    transactions.into_iter().find(|details| {
689        details.payment_kind == kind && details.amount == amount && details.status == status
690    })
691}