gateway_tests/
main.rs

1#![deny(clippy::pedantic)]
2
3use std::collections::BTreeMap;
4use std::fs::remove_dir_all;
5use std::ops::ControlFlow;
6use std::path::PathBuf;
7use std::str::FromStr;
8use std::time::Duration;
9use std::{env, ffi};
10
11use anyhow::ensure;
12use clap::{Parser, Subcommand};
13use devimint::cli::cleanup_on_exit;
14use devimint::envs::FM_DATA_DIR_ENV;
15use devimint::federation::Federation;
16use devimint::gatewayd::LdkChainSource;
17use devimint::util::{ProcessManager, poll, poll_with_timeout};
18use devimint::version_constants::{VERSION_0_6_0_ALPHA, VERSION_0_7_0_ALPHA};
19use devimint::{Gatewayd, LightningNode, cli, cmd, util};
20use fedimint_core::config::FederationId;
21use fedimint_core::time::now;
22use fedimint_core::util::backoff_util::aggressive_backoff_long;
23use fedimint_core::util::retry;
24use fedimint_core::{Amount, BitcoinAmountOrAll};
25use fedimint_gateway_common::{
26    FederationInfo, GatewayBalances, GatewayFedConfig, PaymentDetails, PaymentKind, PaymentStatus,
27};
28use fedimint_logging::LOG_TEST;
29use fedimint_testing_core::node_type::LightningNodeType;
30use itertools::Itertools;
31use tracing::{debug, info};
32
33#[derive(Parser)]
34struct GatewayTestOpts {
35    #[clap(subcommand)]
36    test: GatewayTest,
37}
38
39#[derive(Debug, Clone, Subcommand)]
40enum GatewayTest {
41    ConfigTest {
42        #[arg(long = "gw-type")]
43        gateway_type: LightningNodeType,
44    },
45    GatewaydMnemonic {
46        #[arg(long)]
47        old_gatewayd_path: PathBuf,
48        #[arg(long)]
49        new_gatewayd_path: PathBuf,
50        #[arg(long)]
51        old_gateway_cli_path: PathBuf,
52        #[arg(long)]
53        new_gateway_cli_path: PathBuf,
54    },
55    BackupRestoreTest,
56    LiquidityTest,
57    EsploraTest,
58}
59
60#[tokio::main]
61async fn main() -> anyhow::Result<()> {
62    let opts = GatewayTestOpts::parse();
63    match opts.test {
64        GatewayTest::ConfigTest { gateway_type } => Box::pin(config_test(gateway_type)).await,
65        GatewayTest::GatewaydMnemonic {
66            old_gatewayd_path,
67            new_gatewayd_path,
68            old_gateway_cli_path,
69            new_gateway_cli_path,
70        } => {
71            mnemonic_upgrade_test(
72                old_gatewayd_path,
73                new_gatewayd_path,
74                old_gateway_cli_path,
75                new_gateway_cli_path,
76            )
77            .await
78        }
79        GatewayTest::BackupRestoreTest => Box::pin(backup_restore_test()).await,
80        GatewayTest::LiquidityTest => Box::pin(liquidity_test()).await,
81        GatewayTest::EsploraTest => esplora_test().await,
82    }
83}
84
85async fn backup_restore_test() -> anyhow::Result<()> {
86    Box::pin(
87        devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
88            let gw = if devimint::util::supports_lnv2() {
89                dev_fed.gw_ldk_connected().await?
90            } else {
91                dev_fed.gw_lnd_registered().await?
92            };
93
94            let fed = dev_fed.fed().await?;
95            fed.pegin_gateways(10_000_000, vec![gw]).await?;
96
97            let mnemonic = gw.get_mnemonic().await?.mnemonic;
98
99            // Recover without a backup
100            info!(target: LOG_TEST, "Wiping gateway and recovering without a backup...");
101            let ln = gw.ln.clone();
102            let new_gw = stop_and_recover_gateway(
103                process_mgr.clone(),
104                mnemonic.clone(),
105                gw.to_owned(),
106                ln.clone(),
107                fed,
108            )
109            .await?;
110
111            // Recover with a backup
112            info!(target: LOG_TEST, "Wiping gateway and recovering with a backup...");
113            info!(target: LOG_TEST, "Creating backup...");
114            new_gw.backup_to_fed(fed).await?;
115            stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
116
117            info!(target: LOG_TEST, "backup_restore_test successful");
118            Ok(())
119        }),
120    )
121    .await
122}
123
124async fn stop_and_recover_gateway(
125    process_mgr: ProcessManager,
126    mnemonic: Vec<String>,
127    old_gw: Gatewayd,
128    new_ln: LightningNode,
129    fed: &Federation,
130) -> anyhow::Result<Gatewayd> {
131    let gateway_balances =
132        serde_json::from_value::<GatewayBalances>(cmd!(old_gw, "get-balances").out_json().await?)?;
133    let before_onchain_balance = gateway_balances.onchain_balance_sats;
134
135    // Stop the Gateway
136    let gw_type = old_gw.ln.ln_type();
137    let gw_name = old_gw.gw_name.clone();
138    old_gw.terminate().await?;
139    info!(target: LOG_TEST, "Terminated Gateway");
140
141    // Delete the gateway's database
142    let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
143        .expect("Data dir is not set")
144        .parse()
145        .expect("Could not parse data dir");
146    let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
147    remove_dir_all(gw_db)?;
148    info!(target: LOG_TEST, "Deleted the Gateway's database");
149
150    if gw_type == LightningNodeType::Ldk {
151        // Delete LDK's database as well
152        let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
153        remove_dir_all(ldk_data_dir)?;
154        info!(target: LOG_TEST, "Deleted LDK's database");
155    }
156
157    let seed = mnemonic.join(" ");
158    // TODO: Audit that the environment access only happens in single-threaded code.
159    unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
160    let new_gw = Gatewayd::new(&process_mgr, new_ln).await?;
161    let new_mnemonic = new_gw.get_mnemonic().await?.mnemonic;
162    assert_eq!(mnemonic, new_mnemonic);
163    info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
164
165    let federations = serde_json::from_value::<Vec<FederationInfo>>(
166        new_gw.get_info().await?["federations"].clone(),
167    )?;
168    assert_eq!(0, federations.len());
169    info!(target: LOG_TEST, "Verified new Gateway has no federations");
170
171    new_gw.recover_fed(fed).await?;
172
173    let gateway_balances =
174        serde_json::from_value::<GatewayBalances>(cmd!(new_gw, "get-balances").out_json().await?)?;
175    let ecash_balance = gateway_balances
176        .ecash_balances
177        .first()
178        .expect("Should have one joined federation");
179    assert_eq!(
180        10_000_000,
181        ecash_balance.ecash_balance_msats.sats_round_down()
182    );
183    let after_onchain_balance = gateway_balances.onchain_balance_sats;
184    assert_eq!(before_onchain_balance, after_onchain_balance);
185    info!(target: LOG_TEST, "Verified balances after recovery");
186
187    Ok(new_gw)
188}
189
190/// TODO(v0.5.0): We do not need to run the `gatewayd-mnemonic` test from v0.4.0
191/// -> v0.5.0 over and over again. Once we have verified this test passes for
192/// v0.5.0, it can safely be removed.
193async fn mnemonic_upgrade_test(
194    old_gatewayd_path: PathBuf,
195    new_gatewayd_path: PathBuf,
196    old_gateway_cli_path: PathBuf,
197    new_gateway_cli_path: PathBuf,
198) -> anyhow::Result<()> {
199    // TODO: Audit that the environment access only happens in single-threaded code.
200    unsafe { std::env::set_var("FM_GATEWAYD_BASE_EXECUTABLE", old_gatewayd_path) };
201    // TODO: Audit that the environment access only happens in single-threaded code.
202    unsafe { std::env::set_var("FM_GATEWAY_CLI_BASE_EXECUTABLE", old_gateway_cli_path) };
203    // TODO: Audit that the environment access only happens in single-threaded code.
204    unsafe { std::env::set_var("FM_ENABLE_MODULE_LNV2", "0") };
205
206    devimint::run_devfed_test()
207        .call(|dev_fed, process_mgr| async move {
208            let gatewayd_version = util::Gatewayd::version_or_default().await;
209            let gateway_cli_version = util::GatewayCli::version_or_default().await;
210            info!(
211                target: LOG_TEST,
212                gatewayd_version = %gatewayd_version,
213                gateway_cli_version = %gateway_cli_version,
214                "Running gatewayd mnemonic test"
215            );
216
217            let mut gw_lnd = dev_fed.gw_lnd_registered().await?.to_owned();
218            let fed = dev_fed.fed().await?;
219            let federation_id = FederationId::from_str(fed.calculate_federation_id().as_str())?;
220
221            gw_lnd
222                .restart_with_bin(&process_mgr, &new_gatewayd_path, &new_gateway_cli_path)
223                .await?;
224
225            // Verify that we have a legacy federation
226            let mnemonic_response = gw_lnd.get_mnemonic().await?;
227            assert!(
228                mnemonic_response
229                    .legacy_federations
230                    .contains(&federation_id)
231            );
232
233            info!(target: LOG_TEST, "Verified a legacy federation exists");
234
235            // Leave federation
236            gw_lnd.leave_federation(federation_id).await?;
237
238            // Rejoin federation
239            gw_lnd.connect_fed(fed).await?;
240
241            // Verify that the legacy federation is recognized
242            let mnemonic_response = gw_lnd.get_mnemonic().await?;
243            assert!(
244                mnemonic_response
245                    .legacy_federations
246                    .contains(&federation_id)
247            );
248            assert_eq!(mnemonic_response.legacy_federations.len(), 1);
249
250            info!(target: LOG_TEST, "Verified leaving and re-joining preservers legacy federation");
251
252            // Leave federation and delete database to force migration to mnemonic
253            gw_lnd.leave_federation(federation_id).await?;
254
255            let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
256                .expect("Data dir is not set")
257                .parse()
258                .expect("Could not parse data dir");
259            let gw_fed_db = data_dir
260                .join(gw_lnd.gw_name.clone())
261                .join(format!("{federation_id}.db"));
262            remove_dir_all(gw_fed_db)?;
263
264            gw_lnd.connect_fed(fed).await?;
265
266            // Verify that the re-connected federation is not a legacy federation
267            let mnemonic_response = gw_lnd.get_mnemonic().await?;
268            assert!(
269                !mnemonic_response
270                    .legacy_federations
271                    .contains(&federation_id)
272            );
273            assert_eq!(mnemonic_response.legacy_federations.len(), 0);
274
275            info!(target: LOG_TEST, "Verified deleting database will migrate the federation to use mnemonic");
276
277            info!(target: LOG_TEST, "Successfully completed mnemonic upgrade test");
278
279            Ok(())
280        })
281        .await
282}
283
284/// Test that sets and verifies configurations within the gateway
285#[allow(clippy::too_many_lines)]
286async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
287    Box::pin(
288        devimint::run_devfed_test()
289            .num_feds(2)
290            .call(|dev_fed, process_mgr| async move {
291                let gw = match gw_type {
292                    LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
293                    LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
294                };
295
296                // Try to connect to already connected federation
297                let invite_code = dev_fed.fed().await?.invite_code()?;
298                let output = cmd!(gw, "connect-fed", invite_code.clone())
299                    .out_json()
300                    .await;
301                assert!(
302                    output.is_err(),
303                    "Connecting to the same federation succeeded"
304                );
305                info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
306
307                let gatewayd_version = util::Gatewayd::version_or_default().await;
308
309                // Change the routing fees for a specific federation
310                let fed_id = dev_fed.fed().await?.calculate_federation_id();
311                gw.set_federation_routing_fee(fed_id.clone(), 20, 20000)
312                    .await?;
313
314                let lightning_fee = gw.get_lightning_fee(fed_id.clone()).await?;
315                assert_eq!(
316                    lightning_fee.base.msats, 20,
317                    "Federation base msat is not 20"
318                );
319                assert_eq!(
320                    lightning_fee.parts_per_million, 20000,
321                    "Federation proportional millionths is not 20000"
322                );
323                info!(target: LOG_TEST, "Verified per-federation routing fees changed");
324
325                let info_value = cmd!(gw, "info").out_json().await?;
326                let federations = info_value["federations"]
327                    .as_array()
328                    .expect("federations is an array");
329                assert_eq!(
330                    federations.len(),
331                    1,
332                    "Gateway did not have one connected federation"
333                );
334
335                // Get the federation's config and verify it parses correctly
336                let config_val = if gatewayd_version < *VERSION_0_6_0_ALPHA {
337                    cmd!(gw, "config", "--federation-id", fed_id)
338                        .out_json()
339                        .await?
340                } else {
341                    cmd!(gw, "cfg", "client-config", "--federation-id", fed_id)
342                        .out_json()
343                        .await?
344                };
345
346                serde_json::from_value::<GatewayFedConfig>(config_val)?;
347
348                // Spawn new federation
349                let bitcoind = dev_fed.bitcoind().await?;
350                let new_fed = Federation::new(
351                    &process_mgr,
352                    bitcoind.clone(),
353                    false,
354                    false,
355                    1,
356                    "config-test".to_string(),
357                )
358                .await?;
359                let new_fed_id = new_fed.calculate_federation_id();
360                info!(target: LOG_TEST, "Successfully spawned new federation");
361
362                let new_invite_code = new_fed.invite_code()?;
363                cmd!(gw, "connect-fed", new_invite_code.clone())
364                    .out_json()
365                    .await?;
366
367                let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_6_0_ALPHA {
368                    (50000, 5000)
369                } else {
370                    (0, 10000)
371                };
372
373                let lightning_fee = gw.get_lightning_fee(new_fed_id.clone()).await?;
374                assert_eq!(
375                    lightning_fee.base.msats, default_base,
376                    "Default Base msat for new federation was not correct"
377                );
378                assert_eq!(
379                    lightning_fee.parts_per_million, default_ppm,
380                    "Default Base msat for new federation was not correct"
381                );
382
383                info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
384
385                // Peg-in sats to gw for the new fed
386                let pegin_amount = Amount::from_msats(10_000_000);
387                new_fed
388                    .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
389                    .await?;
390
391                // Verify `info` returns multiple federations
392                let info_value = cmd!(gw, "info").out_json().await?;
393                let federations = info_value["federations"]
394                    .as_array()
395                    .expect("federations is an array");
396
397                assert_eq!(
398                    federations.len(),
399                    2,
400                    "Gateway did not have two connected federations"
401                );
402
403                let federation_fake_scids =
404                    serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
405                        info_value
406                            .get("channels")
407                            .or_else(|| info_value.get("federation_fake_scids"))
408                            .expect("field  exists")
409                            .to_owned(),
410                    )
411                    .expect("cannot parse")
412                    .expect("should have scids");
413
414                assert_eq!(
415                    federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
416                    vec![1, 2]
417                );
418
419                let first_fed_info = federations
420                    .iter()
421                    .find(|i| {
422                        *i["federation_id"]
423                            .as_str()
424                            .expect("should parse as str")
425                            .to_string()
426                            == fed_id
427                    })
428                    .expect("Could not find federation");
429
430                let second_fed_info = federations
431                    .iter()
432                    .find(|i| {
433                        *i["federation_id"]
434                            .as_str()
435                            .expect("should parse as str")
436                            .to_string()
437                            == new_fed_id
438                    })
439                    .expect("Could not find federation");
440
441                let first_fed_balance_msat =
442                    serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
443                        .expect("fed should have balance");
444
445                let second_fed_balance_msat =
446                    serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
447                        .expect("fed should have balance");
448
449                assert_eq!(first_fed_balance_msat, Amount::ZERO);
450                assert_eq!(second_fed_balance_msat, pegin_amount);
451
452                leave_federation(gw, fed_id, 1).await?;
453                leave_federation(gw, new_fed_id, 2).await?;
454
455                // Rejoin new federation, verify that the balance is the same
456                let output = cmd!(gw, "connect-fed", new_invite_code.clone())
457                    .out_json()
458                    .await?;
459                let rejoined_federation_balance_msat =
460                    serde_json::from_value::<Amount>(output["balance_msat"].clone())
461                        .expect("fed has balance");
462
463                assert_eq!(second_fed_balance_msat, rejoined_federation_balance_msat);
464
465                info!(target: LOG_TEST, "Gateway configuration test successful");
466                Ok(())
467            }),
468    )
469    .await
470}
471
472/// Test that verifies the various liquidity tools (onchain, lightning, ecash)
473/// work correctly.
474#[allow(clippy::too_many_lines)]
475async fn liquidity_test() -> anyhow::Result<()> {
476    devimint::run_devfed_test()
477        .call(|dev_fed, _process_mgr| async move {
478            let federation = dev_fed.fed().await?;
479
480            if !devimint::util::supports_lnv2() {
481                info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
482                return Ok(());
483            }
484
485            let gw_lnd = dev_fed.gw_lnd_registered().await?;
486            let gw_ldk = dev_fed.gw_ldk_connected().await?;
487            let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
488            let gateways = [gw_lnd, gw_ldk].to_vec();
489
490            let gateway_matrix = gateways
491                .iter()
492                .cartesian_product(gateways.iter())
493                .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
494
495            info!(target: LOG_TEST, "Pegging-in gateways...");
496            federation
497                .pegin_gateways(1_000_000, gateways.clone())
498                .await?;
499
500            info!(target: LOG_TEST, "Testing ecash payments between gateways...");
501            for (gw_send, gw_receive) in gateway_matrix.clone() {
502                info!(
503                    target: LOG_TEST,
504                    gw_send = %gw_send.ln.ln_type(),
505                    gw_receive = %gw_receive.ln.ln_type(),
506                    "Testing ecash payment",
507                );
508
509                let fed_id = federation.calculate_federation_id();
510                let prev_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
511                let prev_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
512                let ecash = gw_send.send_ecash(fed_id.clone(), 500_000).await?;
513                gw_receive.receive_ecash(ecash).await?;
514                let after_send_ecash_balance = gw_send.ecash_balance(fed_id.clone()).await?;
515                let after_receive_ecash_balance = gw_receive.ecash_balance(fed_id.clone()).await?;
516                assert_eq!(prev_send_ecash_balance - 500_000, after_send_ecash_balance);
517                assert_eq!(
518                    prev_receive_ecash_balance + 500_000,
519                    after_receive_ecash_balance
520                );
521            }
522
523            info!(target: LOG_TEST, "Testing payments between gateways...");
524            for (gw_send, gw_receive) in gateway_matrix.clone() {
525                info!(
526                    target: LOG_TEST,
527                    gw_send = %gw_send.ln.ln_type(),
528                    gw_receive = %gw_receive.ln.ln_type(),
529                    "Testing lightning payment",
530                );
531
532                let invoice = gw_receive.create_invoice(1_000_000).await?;
533                gw_send.pay_invoice(invoice).await?;
534            }
535
536            if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
537                let start = now() - Duration::from_secs(5 * 60);
538                let end = now() + Duration::from_secs(5 * 60);
539                info!(target: LOG_TEST, "Verifying list of transactions");
540                let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
541                // One inbound and one outbound transaction
542                assert_eq!(lnd_transactions.len(), 2);
543
544                let ldk_transactions = gw_ldk.list_transactions(start, end).await?;
545                assert_eq!(ldk_transactions.len(), 2);
546
547                // Verify that transactions are filtered by time
548                let start = now() - Duration::from_secs(10 * 60);
549                let end = now() - Duration::from_secs(5 * 60);
550                let lnd_transactions = gw_lnd.list_transactions(start, end).await?;
551                assert_eq!(lnd_transactions.len(), 0);
552            }
553
554            info!(target: LOG_TEST, "Testing paying through LND Gateway...");
555            // Need to try to pay the invoice multiple times in case the channel graph has
556            // not been updated yet. LDK's channel graph updates slowly in these
557            // tests, so we need to try for awhile.
558            poll_with_timeout("LDK2 pay LDK", Duration::from_secs(180), || async {
559                debug!(target: LOG_TEST, "Trying LDK2 -> LND -> LDK...");
560                let invoice = gw_ldk
561                    .create_invoice(1_550_000)
562                    .await
563                    .map_err(ControlFlow::Continue)?;
564                gw_ldk_second
565                    .pay_invoice(invoice.clone())
566                    .await
567                    .map_err(ControlFlow::Continue)?;
568                Ok(())
569            })
570            .await?;
571
572            if devimint::util::Gatewayd::version_or_default().await >= *VERSION_0_7_0_ALPHA {
573                info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
574                // TODO: investigate why the first BOLT12 payment attempt is expiring consistently
575                poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
576                    let offer_with_amount = gw_ldk_second.create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
577                    gw_ldk.pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
578                    assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
579                    Ok(())
580                }).await?;
581
582                let offer_without_amount = gw_ldk.create_offer(None).await?;
583                gw_ldk_second.pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
584                assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
585
586                // Cannot pay an offer without an amount without specifying an amount
587                gw_ldk_second.pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
588
589                // Verify we can pay the offer again
590                gw_ldk_second.pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
591                assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
592            }
593
594            info!(target: LOG_TEST, "Pegging-out gateways...");
595            federation
596                .pegout_gateways(500_000_000, gateways.clone())
597                .await?;
598
599            info!(target: LOG_TEST, "Testing closing all channels...");
600            gw_ldk_second.close_all_channels().await?;
601            gw_ldk.close_all_channels().await?;
602            let bitcoind = dev_fed.bitcoind().await?;
603            // Need to mine enough blocks in case the channels are force closed.
604            bitcoind.mine_blocks(2016).await?;
605            let block_height = bitcoind.get_block_count().await? - 1;
606            gw_ldk_second.wait_for_block_height(block_height).await?;
607            check_empty_lightning_balance(gw_ldk_second).await?;
608            gw_ldk.wait_for_block_height(block_height).await?;
609            check_empty_lightning_balance(gw_ldk).await?;
610            check_empty_lightning_balance(gw_lnd).await?;
611
612            info!(target: LOG_TEST, "Testing sending onchain...");
613            // TODO: LND is sometimes keeping 10000 sats as reserve when sending onchain
614            // For now we will skip checking LND.
615            gw_ldk
616                .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
617                .await?;
618            gw_ldk_second
619                .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
620                .await?;
621            check_empty_onchain_balance(gw_ldk).await?;
622            check_empty_onchain_balance(gw_ldk_second).await?;
623
624            Ok(())
625        })
626        .await
627}
628
629/// This test cannot be run in CI because it connects to an external esplora
630/// server
631async fn esplora_test() -> anyhow::Result<()> {
632    let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
633    let (process_mgr, task_group) = cli::setup(args).await?;
634    cleanup_on_exit(
635        async {
636            // Spawn mutinynet esplora Gatewayd instance
637            unsafe {
638                std::env::set_var("FM_GATEWAY_NETWORK", "signet");
639                std::env::set_var("FM_LDK_NETWORK", "signet");
640            }
641            let ldk = Gatewayd::new(
642                &process_mgr,
643                LightningNode::Ldk {
644                    name: "gateway-ldk-mutinynet".to_string(),
645                    gw_port: process_mgr.globals.FM_PORT_GW_LDK,
646                    ldk_port: process_mgr.globals.FM_PORT_LDK,
647                    chain_source: LdkChainSource::Esplora,
648                },
649            )
650            .await?;
651
652            poll("Waiting for LDK to be ready", || async {
653                let info = ldk.get_info().await.map_err(ControlFlow::Continue)?;
654                let state: String = serde_json::from_value(info["gateway_state"].clone())
655                    .expect("Could not get gateway state");
656                if state == "Running" {
657                    Ok(())
658                } else {
659                    Err(ControlFlow::Continue(anyhow::anyhow!(
660                        "Gateway not running"
661                    )))
662                }
663            })
664            .await?;
665
666            ldk.get_ln_onchain_address().await?;
667            info!(target:LOG_TEST, "Successfully connected to mutinynet esplora");
668            Ok(())
669        },
670        task_group,
671    )
672    .await?;
673    Ok(())
674}
675
676async fn get_transaction(
677    gateway: &Gatewayd,
678    kind: PaymentKind,
679    amount: Amount,
680    status: PaymentStatus,
681) -> Option<PaymentDetails> {
682    let transactions = gateway
683        .list_transactions(
684            now() - Duration::from_secs(5 * 60),
685            now() + Duration::from_secs(5 * 60),
686        )
687        .await
688        .ok()?;
689    transactions.into_iter().find(|details| {
690        details.payment_kind == kind && details.amount == amount && details.status == status
691    })
692}
693
694async fn check_empty_onchain_balance(gw: &Gatewayd) -> anyhow::Result<()> {
695    retry(
696        "Wait for onchain balance update",
697        aggressive_backoff_long(),
698        || async {
699            let curr_balance = gw.get_balances().await?.onchain_balance_sats;
700            let gw_name = gw.gw_name.clone();
701            ensure!(
702                curr_balance == 0,
703                "Gateway onchain balance is not empty: {curr_balance} gw_name: {gw_name}"
704            );
705            Ok(())
706        },
707    )
708    .await
709}
710
711async fn check_empty_lightning_balance(gw: &Gatewayd) -> anyhow::Result<()> {
712    let balances = gw.get_balances().await?;
713    let curr_lightning_balance = balances.lightning_balance_msats;
714    ensure!(
715        curr_lightning_balance == 0,
716        "Close channels did not sweep all lightning funds"
717    );
718    let inbound_lightning_balance = balances.inbound_lightning_liquidity_msats;
719    ensure!(
720        inbound_lightning_balance == 0,
721        "Close channels did not sweep all lightning funds"
722    );
723    Ok(())
724}
725
726/// Leaves the specified federation by issuing a `leave-fed` POST request to the
727/// gateway.
728async fn leave_federation(gw: &Gatewayd, fed_id: String, expected_scid: u64) -> anyhow::Result<()> {
729    let gatewayd_version = util::Gatewayd::version_or_default().await;
730    let leave_fed = cmd!(gw, "leave-fed", "--federation-id", fed_id.clone())
731        .out_json()
732        .await
733        .expect("Leaving the federation failed");
734
735    let federation_id: FederationId = serde_json::from_value(leave_fed["federation_id"].clone())?;
736    assert_eq!(federation_id.to_string(), fed_id);
737
738    let scid = if gatewayd_version < *VERSION_0_6_0_ALPHA {
739        serde_json::from_value::<u64>(leave_fed["federation_index"].clone())?
740    } else {
741        serde_json::from_value::<u64>(leave_fed["config"]["federation_index"].clone())?
742    };
743
744    assert_eq!(scid, expected_scid);
745
746    info!(target: LOG_TEST, federation_id = %fed_id, "Verified gateway left federation");
747    Ok(())
748}