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