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 clap::{Parser, Subcommand};
12use devimint::cli::cleanup_on_exit;
13use devimint::envs::FM_DATA_DIR_ENV;
14use devimint::external::{Bitcoind, Esplora};
15use devimint::federation::Federation;
16use devimint::util::{ProcessManager, almost_equal, poll, poll_with_timeout};
17use devimint::version_constants::{VERSION_0_8_2, VERSION_0_10_0_ALPHA};
18use devimint::{Gatewayd, LightningNode, cli, util};
19use fedimint_core::config::FederationId;
20use fedimint_core::time::now;
21use fedimint_core::{Amount, BitcoinAmountOrAll, bitcoin, default_esplora_server};
22use fedimint_gateway_common::{FederationInfo, PaymentDetails, PaymentKind, PaymentStatus};
23use fedimint_logging::LOG_TEST;
24use fedimint_testing_core::node_type::LightningNodeType;
25use itertools::Itertools;
26use tracing::info;
27
28#[derive(Parser)]
29struct GatewayTestOpts {
30 #[clap(subcommand)]
31 test: GatewayTest,
32}
33
34#[derive(Debug, Clone, Subcommand)]
35#[allow(clippy::enum_variant_names)]
36enum GatewayTest {
37 ConfigTest {
38 #[arg(long = "gw-type")]
39 gateway_type: LightningNodeType,
40 },
41 BackupRestoreTest,
42 LiquidityTest,
43 EsploraTest,
44}
45
46#[tokio::main]
47async fn main() -> anyhow::Result<()> {
48 let opts = GatewayTestOpts::parse();
49 match opts.test {
50 GatewayTest::ConfigTest { gateway_type } => Box::pin(config_test(gateway_type)).await,
51 GatewayTest::BackupRestoreTest => Box::pin(backup_restore_test()).await,
52 GatewayTest::LiquidityTest => Box::pin(liquidity_test()).await,
53 GatewayTest::EsploraTest => esplora_test().await,
54 }
55}
56
57async fn backup_restore_test() -> anyhow::Result<()> {
58 Box::pin(
59 devimint::run_devfed_test().call(|dev_fed, process_mgr| async move {
60 let gw = if devimint::util::supports_lnv2() {
61 dev_fed.gw_ldk_connected().await?
62 } else {
63 dev_fed.gw_lnd_registered().await?
64 };
65
66 let fed = dev_fed.fed().await?;
67 fed.pegin_gateways(10_000_000, vec![gw]).await?;
68
69 let mnemonic = gw.client().get_mnemonic().await?.mnemonic;
70
71 info!(target: LOG_TEST, "Wiping gateway and recovering without a backup...");
73 let ln = gw.ln.clone();
74 let new_gw = stop_and_recover_gateway(
75 process_mgr.clone(),
76 mnemonic.clone(),
77 gw.to_owned(),
78 ln.clone(),
79 fed,
80 )
81 .await?;
82
83 info!(target: LOG_TEST, "Wiping gateway and recovering with a backup...");
85 info!(target: LOG_TEST, "Creating backup...");
86 new_gw.client().backup_to_fed(fed).await?;
87 stop_and_recover_gateway(process_mgr, mnemonic, new_gw, ln, fed).await?;
88
89 info!(target: LOG_TEST, "backup_restore_test successful");
90 Ok(())
91 }),
92 )
93 .await
94}
95
96async fn stop_and_recover_gateway(
97 process_mgr: ProcessManager,
98 mnemonic: Vec<String>,
99 old_gw: Gatewayd,
100 new_ln: LightningNode,
101 fed: &Federation,
102) -> anyhow::Result<Gatewayd> {
103 let gateway_balances = old_gw.client().get_balances().await?;
104 let before_onchain_balance = gateway_balances.onchain_balance_sats;
105
106 let gw_type = old_gw.ln.ln_type();
108 let gw_name = old_gw.gw_name.clone();
109 let old_gw_index = old_gw.gateway_index;
110 old_gw.terminate().await?;
111 info!(target: LOG_TEST, "Terminated Gateway");
112
113 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
115 .expect("Data dir is not set")
116 .parse()
117 .expect("Could not parse data dir");
118 let gw_db = data_dir.join(gw_name.clone()).join("gatewayd.db");
119 if gw_db.is_file() {
120 remove_file(gw_db)?;
122 } else {
123 remove_dir_all(gw_db)?;
124 }
125 info!(target: LOG_TEST, "Deleted the Gateway's database");
126
127 if gw_type == LightningNodeType::Ldk {
128 let ldk_data_dir = data_dir.join(gw_name).join("ldk_node");
130 remove_dir_all(ldk_data_dir)?;
131 info!(target: LOG_TEST, "Deleted LDK's database");
132 }
133
134 let seed = mnemonic.join(" ");
135 unsafe { std::env::set_var("FM_GATEWAY_MNEMONIC", seed) };
137 let new_gw = Gatewayd::new(&process_mgr, new_ln, old_gw_index).await?;
138 let new_mnemonic = new_gw.client().get_mnemonic().await?.mnemonic;
139 assert_eq!(mnemonic, new_mnemonic);
140 info!(target: LOG_TEST, "Verified mnemonic is the same after creating new Gateway");
141
142 let federations = serde_json::from_value::<Vec<FederationInfo>>(
143 new_gw.client().get_info().await?["federations"].clone(),
144 )?;
145 assert_eq!(0, federations.len());
146 info!(target: LOG_TEST, "Verified new Gateway has no federations");
147
148 new_gw.client().recover_fed(fed).await?;
149
150 let gateway_balances = new_gw.client().get_balances().await?;
151 let ecash_balance = gateway_balances
152 .ecash_balances
153 .first()
154 .expect("Should have one joined federation");
155 almost_equal(
156 ecash_balance.ecash_balance_msats.sats_round_down(),
157 10_000_000,
158 10,
159 )
160 .unwrap();
161 let after_onchain_balance = gateway_balances.onchain_balance_sats;
162 assert_eq!(before_onchain_balance, after_onchain_balance);
163 info!(target: LOG_TEST, "Verified balances after recovery");
164
165 Ok(new_gw)
166}
167
168#[allow(clippy::too_many_lines)]
170async fn config_test(gw_type: LightningNodeType) -> anyhow::Result<()> {
171 Box::pin(
172 devimint::run_devfed_test()
173 .num_feds(2)
174 .call(|dev_fed, process_mgr| async move {
175 let gw = match gw_type {
176 LightningNodeType::Lnd => dev_fed.gw_lnd_registered().await?,
177 LightningNodeType::Ldk => dev_fed.gw_ldk_connected().await?,
178 };
179
180 let invite_code = dev_fed.fed().await?.invite_code()?;
182 gw.client().connect_fed(invite_code).await.expect_err("Connecting to the same federation succeeded");
183 info!(target: LOG_TEST, "Verified that gateway couldn't connect to already connected federation");
184
185 let gatewayd_version = util::Gatewayd::version_or_default().await;
186
187 let fed_id = dev_fed.fed().await?.calculate_federation_id();
189 gw.client().set_federation_routing_fee(fed_id.clone(), 20, 20000)
190 .await?;
191
192 let lightning_fee = gw.client().get_lightning_fee(fed_id.clone()).await?;
193 assert_eq!(
194 lightning_fee.base.msats, 20,
195 "Federation base msat is not 20"
196 );
197 assert_eq!(
198 lightning_fee.parts_per_million, 20000,
199 "Federation proportional millionths is not 20000"
200 );
201 info!(target: LOG_TEST, "Verified per-federation routing fees changed");
202
203 let info_value = gw.client().get_info().await?;
204 let federations = info_value["federations"]
205 .as_array()
206 .expect("federations is an array");
207 assert_eq!(
208 federations.len(),
209 1,
210 "Gateway did not have one connected federation"
211 );
212
213 gw.client().client_config(fed_id.clone()).await?;
215
216 let bitcoind = dev_fed.bitcoind().await?;
218 let new_fed = Federation::new(
219 &process_mgr,
220 bitcoind.clone(),
221 false,
222 false,
223 1,
224 "config-test".to_string(),
225 )
226 .await?;
227 let new_fed_id = new_fed.calculate_federation_id();
228 info!(target: LOG_TEST, "Successfully spawned new federation");
229
230 let new_invite_code = new_fed.invite_code()?;
231 gw.client().connect_fed(new_invite_code.clone()).await?;
232
233 let (default_base, default_ppm) = if gatewayd_version >= *VERSION_0_8_2 {
234 (0, 0)
235 } else {
236 (2000, 3000)
238 };
239
240 let lightning_fee = gw.client().get_lightning_fee(new_fed_id.clone()).await?;
241 assert_eq!(
242 lightning_fee.base.msats, default_base,
243 "Default Base msat for new federation was not correct"
244 );
245 assert_eq!(
246 lightning_fee.parts_per_million, default_ppm,
247 "Default Base msat for new federation was not correct"
248 );
249
250 info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
251
252 let pegin_amount = Amount::from_msats(10_000_000);
254 new_fed
255 .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
256 .await?;
257
258 let info_value = gw.client().get_info().await?;
260 let federations = info_value["federations"]
261 .as_array()
262 .expect("federations is an array");
263
264 assert_eq!(
265 federations.len(),
266 2,
267 "Gateway did not have two connected federations"
268 );
269
270 let federation_fake_scids =
271 serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
272 info_value
273 .get("channels")
274 .or_else(|| info_value.get("federation_fake_scids"))
275 .expect("field exists")
276 .to_owned(),
277 )
278 .expect("cannot parse")
279 .expect("should have scids");
280
281 assert_eq!(
282 federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
283 vec![1, 2]
284 );
285
286 let first_fed_info = federations
287 .iter()
288 .find(|i| {
289 *i["federation_id"]
290 .as_str()
291 .expect("should parse as str")
292 .to_string()
293 == fed_id
294 })
295 .expect("Could not find federation");
296
297 let second_fed_info = federations
298 .iter()
299 .find(|i| {
300 *i["federation_id"]
301 .as_str()
302 .expect("should parse as str")
303 .to_string()
304 == new_fed_id
305 })
306 .expect("Could not find federation");
307
308 let first_fed_balance_msat =
309 serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
310 .expect("fed should have balance");
311
312 let second_fed_balance_msat =
313 serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
314 .expect("fed should have balance");
315
316 assert_eq!(first_fed_balance_msat, Amount::ZERO);
317 almost_equal(second_fed_balance_msat.msats, pegin_amount.msats, 10_000).unwrap();
318
319 let fed_id = FederationId::from_str(&fed_id).expect("invalid Federation ID");
320 let fed_info = gw.client().leave_federation(fed_id).await?;
321 assert_eq!(serde_json::from_value::<FederationId>(fed_info["federation_id"].clone())?, fed_id);
322 assert_eq!(fed_info["config"]["federation_index"].as_u64().expect("Was not u64"), 1);
323 gw.client().leave_federation(fed_id).await.expect_err("Successfully left a federation twice");
324
325 let new_fed_id = FederationId::from_str(&new_fed_id).expect("invalid Federation ID");
326 let fed_info = gw.client().leave_federation(new_fed_id).await?;
327 assert_eq!(serde_json::from_value::<FederationId>(fed_info["federation_id"].clone())?, new_fed_id);
328 assert_eq!(fed_info["config"]["federation_index"].as_u64().expect("Was not u64"), 2);
329
330 let fed_info = gw.client().connect_fed(new_invite_code).await?;
332 assert_eq!(second_fed_balance_msat, Amount::from_msats(fed_info["balance_msat"].as_u64().expect("Balance should be present")));
333
334 if gw.gatewayd_version >= *VERSION_0_10_0_ALPHA {
335 info!(target: LOG_TEST, gatewayd_version = %gw.gatewayd_version, "Getting info over iroh");
337 gw.client().with_iroh().get_info().await?;
338 }
339
340 info!(target: LOG_TEST, "Gateway configuration test successful");
341 Ok(())
342 }),
343 )
344 .await
345}
346
347#[allow(clippy::too_many_lines)]
350async fn liquidity_test() -> anyhow::Result<()> {
351 devimint::run_devfed_test()
352 .call(|dev_fed, _process_mgr| async move {
353 let federation = dev_fed.fed().await?;
354
355 if !devimint::util::supports_lnv2() {
356 info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
357 return Ok(());
358 }
359
360 let gw_lnd = dev_fed.gw_lnd_registered().await?;
361 let gw_ldk = dev_fed.gw_ldk_connected().await?;
362 let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
363 let gateways = [gw_lnd, gw_ldk].to_vec();
364
365 let gateway_matrix = gateways
366 .iter()
367 .cartesian_product(gateways.iter())
368 .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
369
370 info!(target: LOG_TEST, "Pegging-in gateways...");
371 federation
372 .pegin_gateways(1_000_000, gateways.clone())
373 .await?;
374
375 info!(target: LOG_TEST, "Testing ecash payments between gateways...");
376 for (gw_send, gw_receive) in gateway_matrix.clone() {
377 info!(
378 target: LOG_TEST,
379 gw_send = %gw_send.ln.ln_type(),
380 gw_receive = %gw_receive.ln.ln_type(),
381 "Testing ecash payment",
382 );
383
384 let fed_id = federation.calculate_federation_id();
385 let prev_send_ecash_balance = gw_send.client().ecash_balance(fed_id.clone()).await?;
386 let prev_receive_ecash_balance = gw_receive.client().ecash_balance(fed_id.clone()).await?;
387 let ecash = gw_send.client().send_ecash(fed_id.clone(), 500_000).await?;
388 gw_receive.client().receive_ecash(ecash).await?;
389 let after_send_ecash_balance = gw_send.client().ecash_balance(fed_id.clone()).await?;
390 almost_equal(
391 prev_send_ecash_balance - 500_000,
392 after_send_ecash_balance,
393 if util::supports_mint_v2() { 2_000 } else { 512 },
394 )
395 .expect("Balances were not almost equal");
396
397 poll_with_timeout(
398 "receive ecash balance",
399 Duration::from_secs(30),
400 || async {
401 let balance = gw_receive.client().ecash_balance(fed_id.clone()).await
402 .map_err(ControlFlow::Break)?;
403 almost_equal(prev_receive_ecash_balance + 500_000, balance, 2_000)
404 .map_err(|e| ControlFlow::Continue(anyhow::anyhow!(e)))
405 },
406 )
407 .await?;
408 }
409
410 info!(target: LOG_TEST, "Testing payments between gateways...");
411 for (gw_send, gw_receive) in gateway_matrix.clone() {
412 info!(
413 target: LOG_TEST,
414 gw_send = %gw_send.ln.ln_type(),
415 gw_receive = %gw_receive.ln.ln_type(),
416 "Testing lightning payment",
417 );
418
419 let invoice = gw_receive.client().create_invoice(1_000_000).await?;
420 gw_send.client().pay_invoice(invoice).await?;
421 }
422
423 let start = now() - Duration::from_mins(5);
424 let end = now() + Duration::from_mins(5);
425 info!(target: LOG_TEST, "Verifying list of transactions");
426 let lnd_transactions = gw_lnd.client().list_transactions(start, end).await?;
427 assert_eq!(lnd_transactions.len(), 2);
429
430 let ldk_transactions = gw_ldk.client().list_transactions(start, end).await?;
431 assert_eq!(ldk_transactions.len(), 2);
432
433 let start = now() - Duration::from_mins(10);
435 let end = now() - Duration::from_mins(5);
436 let lnd_transactions = gw_lnd.client().list_transactions(start, end).await?;
437 assert_eq!(lnd_transactions.len(), 0);
438
439 info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
440 poll_with_timeout("First BOLT12 payment", Duration::from_secs(30), || async {
442 let offer_with_amount = gw_ldk_second.client().create_offer(Some(Amount::from_msats(10_000_000))).await.map_err(ControlFlow::Continue)?;
443 gw_ldk.client().pay_offer(offer_with_amount, None).await.map_err(ControlFlow::Continue)?;
444 assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
445 Ok(())
446 }).await?;
447
448 let offer_without_amount = gw_ldk.client().create_offer(None).await?;
449 gw_ldk_second.client().pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
450 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
451
452 gw_ldk_second.client().pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
454
455 gw_ldk_second.client().pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
457 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
458
459 info!(target: LOG_TEST, "Pegging-out gateways...");
460 federation
461 .pegout_gateways(500_000_000, gateways.clone())
462 .await?;
463
464 info!(target: LOG_TEST, "Testing only admin can send onchain...");
465 let send_result = gw_lnd.client().with_password("secondbest").send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10).await;
466 assert!(send_result.is_err(), "Only admins can send onchain");
467
468 info!(target: LOG_TEST, "Testing sending onchain...");
469 let bitcoind = dev_fed.bitcoind().await?;
470 for gw in &gateways {
471 let txid = gw
472 .client()
473 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
474 .await?;
475 bitcoind.poll_get_transaction(txid).await?;
476 }
477
478 info!(target: LOG_TEST, "Testing closing all channels...");
479
480 let gw_ldk_pubkey = gw_ldk.client().lightning_pubkey().await?;
482 gw_lnd.client().close_channel(gw_ldk_pubkey, false).await?;
483
484 gw_ldk_second.client().close_all_channels(true).await?;
486
487 for gw in gateways {
489 let channels = gw.client().list_channels().await?;
490 let active_channel = channels.into_iter().any(|chan| chan.is_active);
491 assert!(!active_channel);
492 }
493
494 Ok(())
495 })
496 .await
497}
498
499async fn esplora_test() -> anyhow::Result<()> {
500 let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
501 let (process_mgr, task_group) = cli::setup(args).await?;
502 cleanup_on_exit(
503 async {
504 info!("Spawning bitcoind...");
505 let bitcoind = Bitcoind::new(&process_mgr, false).await?;
506 info!("Spawning esplora...");
507 let _esplora = Esplora::new(&process_mgr, bitcoind).await?;
508 let network = bitcoin::Network::from_str(&process_mgr.globals.FM_GATEWAY_NETWORK)
509 .expect("Could not parse network");
510 let esplora_port = process_mgr.globals.FM_PORT_ESPLORA.to_string();
511 let esplora = default_esplora_server(network, Some(esplora_port));
512 unsafe {
513 std::env::remove_var("FM_BITCOIND_URL");
514 std::env::set_var("FM_ESPLORA_URL", esplora.url.to_string());
515 }
516 info!("Spawning ldk gateway...");
517 let ldk = Gatewayd::new(
518 &process_mgr,
519 LightningNode::Ldk {
520 name: "gateway-ldk-esplora".to_string(),
521 gw_port: process_mgr.globals.FM_PORT_GW_LDK,
522 ldk_port: process_mgr.globals.FM_PORT_LDK,
523 metrics_port: process_mgr.globals.FM_PORT_GW_LDK_METRICS,
524 },
525 0,
526 )
527 .await?;
528
529 info!("Waiting for ldk gatewy to be ready...");
530 poll("Waiting for LDK to be ready", || async {
531 let info = ldk
532 .client()
533 .get_info()
534 .await
535 .map_err(ControlFlow::Continue)?;
536 let state: String = serde_json::from_value(info["gateway_state"].clone())
537 .expect("Could not get gateway state");
538 if state == "Running" {
539 Ok(())
540 } else {
541 Err(ControlFlow::Continue(anyhow::anyhow!(
542 "Gateway not running"
543 )))
544 }
545 })
546 .await?;
547
548 ldk.client().get_ln_onchain_address().await?;
549 info!(target:LOG_TEST, "ldk gateway successfully spawned and connected to esplora");
550 Ok(())
551 },
552 task_group,
553 )
554 .await?;
555 Ok(())
556}
557
558async fn get_transaction(
559 gateway: &Gatewayd,
560 kind: PaymentKind,
561 amount: Amount,
562 status: PaymentStatus,
563) -> Option<PaymentDetails> {
564 let transactions = gateway
565 .client()
566 .list_transactions(
567 now() - Duration::from_mins(5),
568 now() + Duration::from_mins(5),
569 )
570 .await
571 .ok()?;
572 transactions.into_iter().find(|details| {
573 details.payment_kind == kind && details.amount == amount && details.status == status
574 })
575}