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