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_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 fed_id = dev_fed.fed().await?.calculate_federation_id();
187 gw.client().set_federation_routing_fee(fed_id.clone(), 20, 20000)
188 .await?;
189
190 let lightning_fee = gw.client().get_lightning_fee(fed_id.clone()).await?;
191 assert_eq!(
192 lightning_fee.base.msats, 20,
193 "Federation base msat is not 20"
194 );
195 assert_eq!(
196 lightning_fee.parts_per_million, 20000,
197 "Federation proportional millionths is not 20000"
198 );
199 info!(target: LOG_TEST, "Verified per-federation routing fees changed");
200
201 let info_value = gw.client().get_info().await?;
202 let federations = info_value["federations"]
203 .as_array()
204 .expect("federations is an array");
205 assert_eq!(
206 federations.len(),
207 1,
208 "Gateway did not have one connected federation"
209 );
210
211 gw.client().client_config(fed_id.clone()).await?;
213
214 let bitcoind = dev_fed.bitcoind().await?;
216 let new_fed = Federation::new(
217 &process_mgr,
218 bitcoind.clone(),
219 false,
220 false,
221 1,
222 "config-test".to_string(),
223 )
224 .await?;
225 let new_fed_id = new_fed.calculate_federation_id();
226 info!(target: LOG_TEST, "Successfully spawned new federation");
227
228 let new_invite_code = new_fed.invite_code()?;
229 gw.client().connect_fed(new_invite_code.clone()).await?;
230
231 let default_base = 0;
232 let default_ppm = 0;
233
234 let lightning_fee = gw.client().get_lightning_fee(new_fed_id.clone()).await?;
235 assert_eq!(
236 lightning_fee.base.msats, default_base,
237 "Default Base msat for new federation was not correct"
238 );
239 assert_eq!(
240 lightning_fee.parts_per_million, default_ppm,
241 "Default Base msat for new federation was not correct"
242 );
243
244 info!(target: LOG_TEST, federation_id = %new_fed_id, "Verified new federation");
245
246 let pegin_amount = Amount::from_msats(10_000_000);
248 new_fed
249 .pegin_gateways(pegin_amount.sats_round_down(), vec![gw])
250 .await?;
251
252 let info_value = gw.client().get_info().await?;
254 let federations = info_value["federations"]
255 .as_array()
256 .expect("federations is an array");
257
258 assert_eq!(
259 federations.len(),
260 2,
261 "Gateway did not have two connected federations"
262 );
263
264 let federation_fake_scids =
265 serde_json::from_value::<Option<BTreeMap<u64, FederationId>>>(
266 info_value
267 .get("channels")
268 .or_else(|| info_value.get("federation_fake_scids"))
269 .expect("field exists")
270 .to_owned(),
271 )
272 .expect("cannot parse")
273 .expect("should have scids");
274
275 assert_eq!(
276 federation_fake_scids.keys().copied().collect::<Vec<u64>>(),
277 vec![1, 2]
278 );
279
280 let first_fed_info = federations
281 .iter()
282 .find(|i| {
283 *i["federation_id"]
284 .as_str()
285 .expect("should parse as str")
286 .to_string()
287 == fed_id
288 })
289 .expect("Could not find federation");
290
291 let second_fed_info = federations
292 .iter()
293 .find(|i| {
294 *i["federation_id"]
295 .as_str()
296 .expect("should parse as str")
297 .to_string()
298 == new_fed_id
299 })
300 .expect("Could not find federation");
301
302 let first_fed_balance_msat =
303 serde_json::from_value::<Amount>(first_fed_info["balance_msat"].clone())
304 .expect("fed should have balance");
305
306 let second_fed_balance_msat =
307 serde_json::from_value::<Amount>(second_fed_info["balance_msat"].clone())
308 .expect("fed should have balance");
309
310 assert_eq!(first_fed_balance_msat, Amount::ZERO);
311 almost_equal(second_fed_balance_msat.msats, pegin_amount.msats, 10_000).unwrap();
312
313 let fed_id = FederationId::from_str(&fed_id).expect("invalid Federation ID");
314 let fed_info = gw.client().leave_federation(fed_id).await?;
315 assert_eq!(serde_json::from_value::<FederationId>(fed_info["federation_id"].clone())?, fed_id);
316 assert_eq!(fed_info["config"]["federation_index"].as_u64().expect("Was not u64"), 1);
317 gw.client().leave_federation(fed_id).await.expect_err("Successfully left a federation twice");
318
319 let new_fed_id = FederationId::from_str(&new_fed_id).expect("invalid Federation ID");
320 let fed_info = gw.client().leave_federation(new_fed_id).await?;
321 assert_eq!(serde_json::from_value::<FederationId>(fed_info["federation_id"].clone())?, new_fed_id);
322 assert_eq!(fed_info["config"]["federation_index"].as_u64().expect("Was not u64"), 2);
323
324 let fed_info = gw.client().connect_fed(new_invite_code).await?;
326 assert_eq!(second_fed_balance_msat, Amount::from_msats(fed_info["balance_msat"].as_u64().expect("Balance should be present")));
327
328 if gw.gatewayd_version >= *VERSION_0_10_0_ALPHA {
329 info!(target: LOG_TEST, gatewayd_version = %gw.gatewayd_version, "Getting info over iroh");
331 gw.client().with_iroh().get_info().await?;
332 }
333
334 info!(target: LOG_TEST, "Gateway configuration test successful");
335 Ok(())
336 }),
337 )
338 .await
339}
340
341#[allow(clippy::too_many_lines)]
344async fn liquidity_test() -> anyhow::Result<()> {
345 devimint::run_devfed_test()
346 .call(|dev_fed, _process_mgr| async move {
347 let federation = dev_fed.fed().await?;
348
349 if !devimint::util::supports_lnv2() {
350 info!(target: LOG_TEST, "LNv2 is not supported, which is necessary for LDK GW and liquidity test");
351 return Ok(());
352 }
353
354 let gw_lnd = dev_fed.gw_lnd_registered().await?;
355 let gw_ldk = dev_fed.gw_ldk_connected().await?;
356 let gw_ldk_second = dev_fed.gw_ldk_second_connected().await?;
357 let gateways = [gw_lnd, gw_ldk].to_vec();
358
359 let gateway_matrix = gateways
360 .iter()
361 .cartesian_product(gateways.iter())
362 .filter(|(a, b)| a.ln.ln_type() != b.ln.ln_type());
363
364 info!(target: LOG_TEST, "Pegging-in gateways...");
365 federation
366 .pegin_gateways(1_000_000, gateways.clone())
367 .await?;
368
369 info!(target: LOG_TEST, "Testing ecash payments between gateways...");
370 for (gw_send, gw_receive) in gateway_matrix.clone() {
371 info!(
372 target: LOG_TEST,
373 gw_send = %gw_send.ln.ln_type(),
374 gw_receive = %gw_receive.ln.ln_type(),
375 "Testing ecash payment",
376 );
377
378 let fed_id = federation.calculate_federation_id();
379 let prev_send_ecash_balance = gw_send.client().ecash_balance(fed_id.clone()).await?;
380 let prev_receive_ecash_balance = gw_receive.client().ecash_balance(fed_id.clone()).await?;
381 let ecash = gw_send.client().send_ecash(fed_id.clone(), 500_000).await?;
382 gw_receive.client().receive_ecash(ecash).await?;
383 let after_send_ecash_balance = gw_send.client().ecash_balance(fed_id.clone()).await?;
384 almost_equal(
385 prev_send_ecash_balance - 500_000,
386 after_send_ecash_balance,
387 if util::supports_mint_v2() { 2_000 } else { 512 },
388 )
389 .expect("Balances were not almost equal");
390
391 poll_with_timeout(
392 "receive ecash balance",
393 Duration::from_secs(30),
394 || async {
395 let balance = gw_receive.client().ecash_balance(fed_id.clone()).await
396 .map_err(ControlFlow::Break)?;
397 almost_equal(prev_receive_ecash_balance + 500_000, balance, 2_000)
398 .map_err(|e| ControlFlow::Continue(anyhow::anyhow!(e)))
399 },
400 )
401 .await?;
402 }
403
404 info!(target: LOG_TEST, "Testing payments between gateways...");
405 for (gw_send, gw_receive) in gateway_matrix.clone() {
406 info!(
407 target: LOG_TEST,
408 gw_send = %gw_send.ln.ln_type(),
409 gw_receive = %gw_receive.ln.ln_type(),
410 "Testing lightning payment",
411 );
412
413 let invoice = gw_receive.client().create_invoice(1_000_000).await?;
414 gw_send.client().pay_invoice(invoice).await?;
415 }
416
417 let start = now() - Duration::from_mins(5);
418 let end = now() + Duration::from_mins(5);
419 info!(target: LOG_TEST, "Verifying list of transactions");
420 let lnd_transactions = gw_lnd.client().list_transactions(start, end).await?;
421 assert_eq!(lnd_transactions.len(), 2);
423
424 let ldk_transactions = gw_ldk.client().list_transactions(start, end).await?;
425 assert_eq!(ldk_transactions.len(), 2);
426
427 let start = now() - Duration::from_mins(10);
429 let end = now() - Duration::from_mins(5);
430 let lnd_transactions = gw_lnd.client().list_transactions(start, end).await?;
431 assert_eq!(lnd_transactions.len(), 0);
432
433 info!(target: LOG_TEST, "Testing paying Bolt12 Offers...");
434 let offer_with_amount = gw_ldk_second.client().create_offer(Some(Amount::from_msats(10_000_000))).await?;
435 gw_ldk.client().pay_offer(offer_with_amount, None).await?;
436 assert!(get_transaction(gw_ldk_second, PaymentKind::Bolt12Offer, Amount::from_msats(10_000_000), PaymentStatus::Succeeded).await.is_some());
437
438 let offer_without_amount = gw_ldk.client().create_offer(None).await?;
439 gw_ldk_second.client().pay_offer(offer_without_amount.clone(), Some(Amount::from_msats(5_000_000))).await?;
440 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(5_000_000), PaymentStatus::Succeeded).await.is_some());
441
442 gw_ldk_second.client().pay_offer(offer_without_amount.clone(), None).await.expect_err("Cannot pay amountless offer without specifying an amount");
444
445 gw_ldk_second.client().pay_offer(offer_without_amount, Some(Amount::from_msats(3_000_000))).await?;
447 assert!(get_transaction(gw_ldk, PaymentKind::Bolt12Offer, Amount::from_msats(3_000_000), PaymentStatus::Succeeded).await.is_some());
448
449 info!(target: LOG_TEST, "Pegging-out gateways...");
450 federation
451 .pegout_gateways(500_000_000, gateways.clone())
452 .await?;
453
454 info!(target: LOG_TEST, "Testing only admin can send onchain...");
455 let send_result = gw_lnd.client().with_password("secondbest").send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10).await;
456 assert!(send_result.is_err(), "Only admins can send onchain");
457
458 info!(target: LOG_TEST, "Testing sending onchain...");
459 let bitcoind = dev_fed.bitcoind().await?;
460 for gw in &gateways {
461 let txid = gw
462 .client()
463 .send_onchain(dev_fed.bitcoind().await?, BitcoinAmountOrAll::All, 10)
464 .await?;
465 bitcoind.poll_get_transaction(txid).await?;
466 }
467
468 info!(target: LOG_TEST, "Testing closing all channels...");
469
470 let gw_ldk_pubkey = gw_ldk.client().lightning_pubkey().await?;
472 gw_lnd.client().close_channel(gw_ldk_pubkey, false).await?;
473
474 for gw in &gateways {
476 gw.client()
477 .close_all_channels(true, Duration::from_secs(30))
478 .await?;
479 }
480
481 Ok(())
482 })
483 .await
484}
485
486async fn esplora_test() -> anyhow::Result<()> {
487 let args = cli::CommonArgs::parse_from::<_, ffi::OsString>(vec![]);
488 let (process_mgr, task_group) = cli::setup(args).await?;
489 cleanup_on_exit(
490 async {
491 info!("Spawning bitcoind...");
492 let bitcoind = Bitcoind::new(&process_mgr, false).await?;
493 info!("Spawning esplora...");
494 let _esplora = Esplora::new(&process_mgr, bitcoind).await?;
495 let network = bitcoin::Network::from_str(&process_mgr.globals.FM_GATEWAY_NETWORK)
496 .expect("Could not parse network");
497 let esplora_port = process_mgr.globals.FM_PORT_ESPLORA.to_string();
498 let esplora = default_esplora_server(network, Some(esplora_port));
499 unsafe {
500 std::env::remove_var("FM_BITCOIND_URL");
501 std::env::set_var("FM_ESPLORA_URL", esplora.url.to_string());
502 }
503 info!("Spawning ldk gateway...");
504 let ldk = Gatewayd::new(
505 &process_mgr,
506 LightningNode::Ldk {
507 name: "gateway-ldk-esplora".to_string(),
508 gw_port: process_mgr.globals.FM_PORT_GW_LDK,
509 ldk_port: process_mgr.globals.FM_PORT_LDK,
510 metrics_port: process_mgr.globals.FM_PORT_GW_LDK_METRICS,
511 },
512 0,
513 )
514 .await?;
515
516 info!("Waiting for ldk gatewy to be ready...");
517 poll("Waiting for LDK to be ready", || async {
518 let info = ldk
519 .client()
520 .get_info()
521 .await
522 .map_err(ControlFlow::Continue)?;
523 let state: String = serde_json::from_value(info["gateway_state"].clone())
524 .expect("Could not get gateway state");
525 if state == "Running" {
526 Ok(())
527 } else {
528 Err(ControlFlow::Continue(anyhow::anyhow!(
529 "Gateway not running"
530 )))
531 }
532 })
533 .await?;
534
535 ldk.client().get_ln_onchain_address().await?;
536 info!(target:LOG_TEST, "ldk gateway successfully spawned and connected to esplora");
537 Ok(())
538 },
539 task_group,
540 )
541 .await?;
542 Ok(())
543}
544
545async fn get_transaction(
546 gateway: &Gatewayd,
547 kind: PaymentKind,
548 amount: Amount,
549 status: PaymentStatus,
550) -> Option<PaymentDetails> {
551 let transactions = gateway
552 .client()
553 .list_transactions(
554 now() - Duration::from_mins(5),
555 now() + Duration::from_mins(5),
556 )
557 .await
558 .ok()?;
559 transactions.into_iter().find(|details| {
560 details.payment_kind == kind && details.amount == amount && details.status == status
561 })
562}