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