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