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