1use std::collections::BTreeMap;
2use std::future::Future;
3use std::ops::ControlFlow;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::time::Duration;
7use std::{env, fs};
8
9use anyhow::{Context, Result, bail};
10use fedimint_api_client::api::{ClientConfigDownloadError, DynGlobalApi};
11use fedimint_api_client::try_download_client_config;
12use fedimint_client_module::module::ClientModule;
13use fedimint_connectors::ConnectorRegistry;
14use fedimint_core::admin_client::SetupStatus;
15use fedimint_core::config::{ClientConfig, load_from_file};
16use fedimint_core::core::{ModuleInstanceId, ModuleKind};
17use fedimint_core::invite_code::InviteCode;
18use fedimint_core::module::ModuleCommon;
19use fedimint_core::module::registry::ModuleDecoderRegistry;
20use fedimint_core::runtime::block_in_place;
21use fedimint_core::task::block_on;
22use fedimint_core::task::jit::JitTry;
23use fedimint_core::util::SafeUrl;
24use fedimint_core::{Amount, NumPeers, PeerId};
25use fedimint_gateway_common::WithdrawResponse;
26use fedimint_logging::LOG_DEVIMINT;
27use fedimint_server::config::io::{CONSENSUS_CONFIG, JSON_EXT, LOCAL_CONFIG, PRIVATE_CONFIG};
28use fedimint_testing_core::config::API_AUTH;
29use fedimint_testing_core::node_type::LightningNodeType;
30use fedimint_wallet_client::WalletClientModule;
31use fedimint_wallet_client::config::WalletClientConfig;
32use fs_lock::FileLock;
33use futures::future::{join_all, try_join_all};
34use tokio::task::{JoinSet, spawn_blocking};
35use tokio::time::Instant;
36use tracing::{debug, info};
37
38use super::external::Bitcoind;
39use super::util::{Command, ProcessHandle, ProcessManager, cmd};
40use super::vars::utf8;
41use crate::envs::{FM_CLIENT_DIR_ENV, FM_DATA_DIR_ENV};
42use crate::util::{FedimintdCmd, poll, poll_simple, poll_with_timeout};
43use crate::version_constants::{VERSION_0_10_0_ALPHA, VERSION_0_11_0_ALPHA, VERSION_0_12_0_ALPHA};
44use crate::{poll_almost_equal, poll_eq, vars};
45
46pub const PORTS_PER_FEDIMINTD: u16 = 4;
49pub const FEDIMINTD_P2P_PORT_OFFSET: u16 = 0;
51pub const FEDIMINTD_API_PORT_OFFSET: u16 = 1;
53pub const FEDIMINTD_UI_PORT_OFFSET: u16 = 2;
55pub const FEDIMINTD_METRICS_PORT_OFFSET: u16 = 3;
57
58async fn query_running_peers<T, Q, F>(members: &BTreeMap<usize, T>, mut query: Q) -> Result<()>
60where
61 Q: FnMut(usize) -> F,
62 F: Future<Output = Result<()>>,
63{
64 for peer_id in members.keys().copied() {
65 query(peer_id)
66 .await
67 .with_context(|| format!("peer {peer_id} is not online"))?;
68 }
69 Ok(())
70}
71
72async fn await_client_config(connectors: &ConnectorRegistry, invite: &InviteCode) -> Result<()> {
73 let api = DynGlobalApi::new(
74 connectors.clone(),
75 invite.peers(),
76 invite.api_secret().as_deref(),
77 );
78 poll("awaiting client config API", || async {
82 match try_download_client_config(
83 connectors,
84 &api,
85 invite.federation_id(),
86 invite.api_secret(),
87 )
88 .await
89 {
90 Ok(_) => Ok(()),
91 Err(err @ ClientConfigDownloadError::Federation(_)) => {
92 Err(ControlFlow::Continue(err.into()))
93 }
94 Err(err) => Err(ControlFlow::Break(err.into())),
95 }
96 })
97 .await
98}
99
100#[derive(Clone)]
101pub struct Federation {
102 pub members: BTreeMap<usize, Fedimintd>,
104 pub vars: BTreeMap<usize, vars::Fedimintd>,
105 pub bitcoind: Bitcoind,
106
107 client: JitTry<Client, anyhow::Error>,
109 #[allow(dead_code)] connectors: ConnectorRegistry,
111}
112
113impl Drop for Federation {
114 fn drop(&mut self) {
115 block_in_place(|| {
116 block_on(async {
117 let mut set = JoinSet::new();
118
119 while let Some((_id, fedimintd)) = self.members.pop_first() {
120 set.spawn(async { drop(fedimintd) });
121 }
122 while (set.join_next().await).is_some() {}
123 });
124 });
125 }
126}
127#[derive(Clone)]
129pub struct Client {
130 name: String,
131}
132
133impl Client {
134 fn clients_dir() -> PathBuf {
135 let data_dir: PathBuf = env::var(FM_DATA_DIR_ENV)
136 .expect("FM_DATA_DIR_ENV not set")
137 .parse()
138 .expect("FM_DATA_DIR_ENV invalid");
139 data_dir.join("clients")
140 }
141
142 fn client_dir(&self) -> PathBuf {
143 Self::clients_dir().join(&self.name)
144 }
145
146 pub fn client_name_lock(name: &str) -> Result<FileLock> {
147 let lock_path = Self::clients_dir().join(format!(".{name}.lock"));
148 let file_lock = std::fs::OpenOptions::new()
149 .write(true)
150 .create(true)
151 .truncate(true)
152 .open(&lock_path)
153 .with_context(|| format!("Failed to open {}", lock_path.display()))?;
154
155 fs_lock::FileLock::new_exclusive(file_lock)
156 .with_context(|| format!("Failed to lock {}", lock_path.display()))
157 }
158
159 pub async fn create(name: impl ToString) -> Result<Client> {
161 let name = name.to_string();
162 spawn_blocking(move || {
163 let _lock = Self::client_name_lock(&name);
164 for i in 0u64.. {
165 let client = Self {
166 name: format!("{name}-{i}"),
167 };
168
169 if !client.client_dir().exists() {
170 std::fs::create_dir_all(client.client_dir())?;
171 return Ok(client);
172 }
173 }
174 unreachable!()
175 })
176 .await?
177 }
178
179 pub fn open_or_create(name: &str) -> Result<Client> {
181 block_in_place(|| {
182 let _lock = Self::client_name_lock(name);
183 let client = Self {
184 name: format!("{name}-0"),
185 };
186 if !client.client_dir().exists() {
187 std::fs::create_dir_all(client.client_dir())?;
188 }
189 Ok(client)
190 })
191 }
192
193 pub async fn join_federation(&self, invite_code: String) -> Result<()> {
195 debug!(target: LOG_DEVIMINT, "Joining federation with the main client");
196 cmd!(self, "join-federation", invite_code).run().await?;
197
198 Ok(())
199 }
200
201 pub async fn restore_federation(&self, invite_code: String, mnemonic: String) -> Result<()> {
203 debug!(target: LOG_DEVIMINT, "Joining federation with restore procedure");
204 cmd!(
205 self,
206 "restore",
207 "--invite-code",
208 invite_code,
209 "--mnemonic",
210 mnemonic
211 )
212 .run()
213 .await?;
214
215 Ok(())
216 }
217
218 pub async fn new_restored(&self, name: &str, invite_code: String) -> Result<Self> {
220 let restored = Self::open_or_create(name)?;
221
222 let mnemonic = cmd!(self, "print-secret").out_json().await?["secret"]
223 .as_str()
224 .unwrap()
225 .to_owned();
226
227 debug!(target: LOG_DEVIMINT, name, "Restoring from mnemonic");
228 cmd!(
229 restored,
230 "restore",
231 "--invite-code",
232 invite_code,
233 "--mnemonic",
234 mnemonic
235 )
236 .run()
237 .await?;
238
239 Ok(restored)
240 }
241
242 pub async fn new_forked(&self, name: impl ToString) -> Result<Client> {
245 let new = Client::create(name).await?;
246
247 cmd!(
248 "cp",
249 "-R",
250 self.client_dir().join("client.db").display(),
251 new.client_dir().display()
252 )
253 .run()
254 .await?;
255
256 Ok(new)
257 }
258
259 pub async fn balance(&self) -> Result<u64> {
260 Ok(cmd!(self, "info").out_json().await?["total_amount_msat"]
261 .as_u64()
262 .unwrap())
263 }
264
265 pub async fn await_balance(&self, min_balance_msat: u64) -> Result<()> {
267 loop {
268 cmd!(self, "dev", "wait", "3").out_json().await?;
269
270 let balance = self.balance().await?;
271 if balance >= min_balance_msat {
272 return Ok(());
273 }
274
275 info!(
276 target: LOG_DEVIMINT,
277 balance,
278 min_balance_msat,
279 "Waiting for client balance to reach minimum"
280 );
281 }
282 }
283
284 pub async fn await_receive(&self, position: &str) -> Result<String> {
289 let output = cmd!(self, "module", "walletv2", "await-receive", position)
290 .out_json()
291 .await?;
292
293 Ok(output[1].to_string())
295 }
296
297 pub async fn get_deposit_addr(&self) -> Result<(String, String)> {
298 if crate::util::supports_wallet_v2() {
299 if crate::util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA {
300 let position = cmd!(self, "dev", "next-event-log-id").out_json().await?;
304
305 let address = cmd!(self, "module", "walletv2", "receive")
306 .out_json()
307 .await?;
308
309 Ok((address.as_str().unwrap().to_string(), position.to_string()))
310 } else {
311 let address = cmd!(self, "module", "walletv2", "receive")
315 .out_json()
316 .await?;
317
318 Ok((address.as_str().unwrap().to_string(), String::new()))
319 }
320 } else {
321 let deposit = cmd!(self, "deposit-address").out_json().await?;
322 Ok((
323 deposit["address"].as_str().unwrap().to_string(),
324 deposit["operation_id"].as_str().unwrap().to_string(),
325 ))
326 }
327 }
328
329 pub async fn await_deposit(&self, operation_id: &str) -> Result<()> {
330 cmd!(self, "await-deposit", operation_id).run().await
331 }
332
333 pub fn cmd(&self) -> Command {
334 cmd!(
335 crate::util::get_fedimint_cli_path(),
336 format!("--data-dir={}", self.client_dir().display())
337 )
338 }
339
340 pub fn get_name(&self) -> &str {
341 &self.name
342 }
343
344 pub async fn get_session_count(&self) -> Result<u64> {
346 cmd!(self, "dev", "session-count").out_json().await?["count"]
347 .as_u64()
348 .context("count field wasn't a number")
349 }
350
351 pub async fn wait_complete(&self) -> Result<()> {
353 cmd!(self, "dev", "wait-complete").run().await
354 }
355
356 pub async fn wait_session(&self) -> anyhow::Result<()> {
358 info!("Waiting for a new session");
359 let session_count = self.get_session_count().await?;
360 self.wait_session_outcome(session_count).await?;
361 Ok(())
362 }
363
364 pub async fn wait_session_outcome(&self, session_count: u64) -> anyhow::Result<()> {
366 let timeout = {
367 let current_session_count = self.get_session_count().await?;
368 let sessions_to_wait = session_count.saturating_sub(current_session_count) + 1;
369 let session_duration_seconds = 180;
370 Duration::from_secs(sessions_to_wait * session_duration_seconds)
371 };
372
373 let start = Instant::now();
374 poll_with_timeout("Waiting for a new session", timeout, || async {
375 info!("Awaiting session outcome {session_count}");
376 match cmd!(self, "dev", "api", "await_session_outcome", session_count)
377 .run()
378 .await
379 {
380 Err(e) => Err(ControlFlow::Continue(e)),
381 Ok(()) => Ok(()),
382 }
383 })
384 .await?;
385
386 let session_found_in = start.elapsed();
387 info!("session found in {session_found_in:?}");
388 Ok(())
389 }
390}
391
392impl Federation {
393 pub async fn new(
394 process_mgr: &ProcessManager,
395 bitcoind: Bitcoind,
396 skip_setup: bool,
397 pre_dkg: bool,
398 pre_restore: bool,
399 fed_index: usize,
401 federation_name: String,
402 ) -> Result<Self> {
403 let num_peers = NumPeers::from(process_mgr.globals.FM_FED_SIZE);
404 let mut members = BTreeMap::new();
405 let mut peer_to_env_vars_map = BTreeMap::new();
406
407 let mut admin_clients: BTreeMap<PeerId, DynGlobalApi> = BTreeMap::new();
408 let mut api_endpoints: BTreeMap<PeerId, _> = BTreeMap::new();
409
410 let connectors = ConnectorRegistry::build_from_testing_env().bind().await;
411 for peer_id in num_peers.peer_ids() {
412 let peer_env_vars = vars::Fedimintd::init(
413 &process_mgr.globals,
414 federation_name.clone(),
415 peer_id,
416 process_mgr
417 .globals
418 .fedimintd_overrides
419 .peer_expect(fed_index, peer_id),
420 )
421 .await?;
422 members.insert(
423 peer_id.to_usize(),
424 Fedimintd::new(
425 process_mgr,
426 bitcoind.clone(),
427 peer_id.to_usize(),
428 &peer_env_vars,
429 federation_name.clone(),
430 )
431 .await?,
432 );
433 let admin_client = DynGlobalApi::new_admin_setup(
434 connectors.clone(),
435 SafeUrl::parse(&peer_env_vars.FM_API_URL)?,
436 );
439 api_endpoints.insert(peer_id, peer_env_vars.FM_API_URL.clone());
440 admin_clients.insert(peer_id, admin_client);
441 peer_to_env_vars_map.insert(peer_id.to_usize(), peer_env_vars);
442 }
443
444 if !skip_setup && !pre_dkg {
445 let (original_fedimint_cli_path, original_fm_mint_client) =
448 crate::util::use_matching_fedimint_cli_for_dkg().await?;
449
450 run_cli_dkg_v2(api_endpoints).await?;
451
452 crate::util::use_fedimint_cli(original_fedimint_cli_path, original_fm_mint_client);
454
455 let client_dir = utf8(&process_mgr.globals.FM_CLIENT_DIR);
457 let invite_code_filename_original = "invite-code";
458
459 for peer_env_vars in peer_to_env_vars_map.values() {
460 let peer_data_dir = utf8(&peer_env_vars.FM_DATA_DIR);
461
462 let invite_code = poll_simple("awaiting-invite-code", || async {
463 let path = format!("{peer_data_dir}/{invite_code_filename_original}");
464 tokio::fs::read_to_string(&path)
465 .await
466 .with_context(|| format!("Awaiting invite code file: {path}"))
467 })
468 .await
469 .context("Awaiting invite code file")?;
470
471 await_client_config(&connectors, &InviteCode::from_str(&invite_code)?).await?;
472 }
473
474 let peer_data_dir = utf8(&peer_to_env_vars_map[&0].FM_DATA_DIR);
476
477 tokio::fs::copy(
478 format!("{peer_data_dir}/{invite_code_filename_original}"),
479 format!("{client_dir}/{invite_code_filename_original}"),
480 )
481 .await
482 .context("copying invite-code file")?;
483
484 for (index, peer_env_vars) in &peer_to_env_vars_map {
487 let peer_data_dir = utf8(&peer_env_vars.FM_DATA_DIR);
488
489 let invite_code_filename_indexed =
490 format!("{invite_code_filename_original}-{index}");
491 tokio::fs::rename(
492 format!("{peer_data_dir}/{invite_code_filename_original}"),
493 format!("{client_dir}/{invite_code_filename_indexed}"),
494 )
495 .await
496 .context("moving invite-code file")?;
497 }
498
499 debug!("Moved invite-code files to client data directory");
500
501 if pre_restore {
502 Self::restart_guardian_for_manual_restore(
503 process_mgr,
504 &mut members,
505 &peer_to_env_vars_map,
506 &bitcoind,
507 )
508 .await?;
509 }
510 }
511
512 let client = JitTry::<_, anyhow::Error>::new_try({
513 move || async move {
514 let client = Client::open_or_create(federation_name.as_str())?;
515 let invite_code = Self::invite_code_static()?;
516 if !skip_setup && !pre_dkg {
517 cmd!(client, "join-federation", invite_code).run().await?;
518 }
519 Ok(client)
520 }
521 });
522
523 Ok(Self {
524 members,
525 vars: peer_to_env_vars_map,
526 bitcoind,
527 client,
528 connectors,
529 })
530 }
531
532 async fn restart_guardian_for_manual_restore(
533 process_mgr: &ProcessManager,
534 members: &mut BTreeMap<usize, Fedimintd>,
535 peer_to_env_vars_map: &BTreeMap<usize, vars::Fedimintd>,
536 bitcoind: &Bitcoind,
537 ) -> Result<()> {
538 const RESTORE_PEER: usize = 0;
539
540 let peer_env_vars = &peer_to_env_vars_map[&RESTORE_PEER];
541 let backup_dir = process_mgr.globals.FM_TEST_DIR.join("fedimintd-backups");
542 tokio::fs::create_dir_all(&backup_dir)
543 .await
544 .context("Creating fedimintd backup directory")?;
545 let backup_path = backup_dir.join("fedimint-0-guardian-backup.tar");
546
547 Self::write_guardian_backup_tar(&peer_env_vars.FM_DATA_DIR, &backup_path).await?;
548 info!(
549 target: LOG_DEVIMINT,
550 path = %backup_path.display(),
551 "Wrote guardian backup for manual restore"
552 );
553
554 let fedimintd = members
555 .remove(&RESTORE_PEER)
556 .context("Missing fedimint-0 process")?;
557 fedimintd.terminate().await?;
558 tokio::fs::remove_dir_all(&peer_env_vars.FM_DATA_DIR)
559 .await
560 .with_context(|| format!("Removing {}", peer_env_vars.FM_DATA_DIR.display()))?;
561 tokio::fs::create_dir_all(&peer_env_vars.FM_DATA_DIR)
562 .await
563 .with_context(|| format!("Creating {}", peer_env_vars.FM_DATA_DIR.display()))?;
564
565 let fedimintd = Fedimintd::new(
566 process_mgr,
567 bitcoind.clone(),
568 RESTORE_PEER,
569 peer_env_vars,
570 "default".to_string(),
571 )
572 .await?;
573 members.insert(RESTORE_PEER, fedimintd);
574
575 info!(
576 target: LOG_DEVIMINT,
577 ui = %peer_env_vars.FM_BIND_UI,
578 backup = %backup_path.display(),
579 "fedimint-0 restarted in setup mode for manual restore"
580 );
581
582 Ok(())
583 }
584
585 async fn write_guardian_backup_tar(data_dir: &Path, backup_path: &Path) -> Result<()> {
586 let data_dir = data_dir.to_path_buf();
587 let backup_path = backup_path.to_path_buf();
588 spawn_blocking(move || {
589 let file = fs::File::options()
590 .write(true)
591 .create_new(true)
592 .open(&backup_path)
593 .with_context(|| format!("Creating {}", backup_path.display()))?;
594 let mut archive = tar::Builder::new(file);
595 for path in [
596 PathBuf::from(LOCAL_CONFIG).with_extension(JSON_EXT),
597 PathBuf::from(CONSENSUS_CONFIG).with_extension(JSON_EXT),
598 PathBuf::from(PRIVATE_CONFIG).with_extension(JSON_EXT),
599 ] {
600 archive
601 .append_path_with_name(data_dir.join(&path), &path)
602 .with_context(|| format!("Adding {} to backup", path.display()))?;
603 }
604 archive.finish().context("Finishing guardian backup tar")?;
605 Ok::<_, anyhow::Error>(())
606 })
607 .await?
608 }
609
610 pub fn client_config(&self) -> Result<ClientConfig> {
611 let cfg_path = self.vars[&0].FM_DATA_DIR.join("client.json");
612 Ok(load_from_file(&cfg_path)?)
613 }
614
615 pub fn module_instance_id_by_kind(&self, kind: &ModuleKind) -> Result<ModuleInstanceId> {
617 self.client_config()?
618 .modules
619 .iter()
620 .find_map(|(id, cfg)| if &cfg.kind == kind { Some(*id) } else { None })
621 .with_context(|| format!("Module kind {kind} not found"))
622 }
623
624 pub fn module_client_config<M: ClientModule>(
625 &self,
626 ) -> Result<Option<<M::Common as ModuleCommon>::ClientConfig>> {
627 self.client_config()?
628 .modules
629 .iter()
630 .find_map(|(module_instance_id, module_cfg)| {
631 if module_cfg.kind == M::kind() {
632 let decoders = ModuleDecoderRegistry::new(vec![(
633 *module_instance_id,
634 M::kind(),
635 M::decoder(),
636 )]);
637 Some(
638 module_cfg
639 .config
640 .clone()
641 .redecode_raw(&decoders)
642 .expect("Decoding client cfg failed")
643 .expect_decoded_ref()
644 .as_any()
645 .downcast_ref::<<M::Common as ModuleCommon>::ClientConfig>()
646 .cloned()
647 .context("Cast to module config failed"),
648 )
649 } else {
650 None
651 }
652 })
653 .transpose()
654 }
655
656 pub fn deposit_fees(&self) -> Result<Amount> {
657 if crate::util::supports_wallet_v2() {
658 Ok(self
659 .module_client_config::<fedimint_walletv2_client::WalletClientModule>()?
660 .context("No walletv2 module found")?
661 .fee_consensus
662 .base)
663 } else {
664 Ok(self
665 .module_client_config::<WalletClientModule>()?
666 .context("No wallet module found")?
667 .fee_consensus
668 .peg_in_abs)
669 }
670 }
671
672 pub fn invite_code(&self) -> Result<String> {
674 let data_dir: PathBuf = env::var(FM_CLIENT_DIR_ENV)?.parse()?;
675 let invite_code = fs::read_to_string(data_dir.join("invite-code"))?;
676 Ok(invite_code)
677 }
678
679 pub fn invite_code_static() -> Result<String> {
680 let data_dir: PathBuf = env::var(FM_CLIENT_DIR_ENV)?.parse()?;
681 let invite_code = fs::read_to_string(data_dir.join("invite-code"))?;
682 Ok(invite_code)
683 }
684 pub fn invite_code_for(peer_id: PeerId) -> Result<String> {
685 let data_dir: PathBuf = env::var(FM_CLIENT_DIR_ENV)?.parse()?;
686 let name = format!("invite-code-{peer_id}");
687 let invite_code = fs::read_to_string(data_dir.join(name))?;
688 Ok(invite_code)
689 }
690
691 pub async fn internal_client(&self) -> Result<&Client> {
695 self.client
696 .get_try()
697 .await
698 .context("Internal client joining Federation")
699 }
700
701 pub async fn new_joined_client(&self, name: impl ToString) -> Result<Client> {
703 let client = Client::create(name).await?;
704 client.join_federation(self.invite_code()?).await?;
705 Ok(client)
706 }
707
708 pub async fn add_lnv2_gateway(&self, gateway: &str) -> Result<()> {
715 let client = self.internal_client().await?;
716 for peer_id in self.members.keys() {
717 poll("adding lnv2 gateway", || async {
721 cmd!(
722 client,
723 "--our-id",
724 peer_id.to_string(),
725 "--password",
726 API_AUTH.as_str(),
727 "module",
728 "lnv2",
729 "gateways",
730 "add",
731 gateway
732 )
733 .run()
734 .await
735 .map_err(ControlFlow::Continue)
736 })
737 .await?;
738 }
739 Ok(())
740 }
741
742 pub async fn start_server(&mut self, process_mgr: &ProcessManager, peer: usize) -> Result<()> {
743 if self.members.contains_key(&peer) {
744 bail!("fedimintd-{peer} already running");
745 }
746 self.members.insert(
747 peer,
748 Fedimintd::new(
749 process_mgr,
750 self.bitcoind.clone(),
751 peer,
752 &self.vars[&peer],
753 "default".to_string(),
754 )
755 .await?,
756 );
757 Ok(())
758 }
759
760 pub async fn terminate_server(&mut self, peer_id: usize) -> Result<()> {
761 let Some((_, fedimintd)) = self.members.remove_entry(&peer_id) else {
762 bail!("fedimintd-{peer_id} does not exist");
763 };
764 fedimintd.terminate().await?;
765 Ok(())
766 }
767
768 pub async fn await_server_terminated(&mut self, peer_id: usize) -> Result<()> {
769 let Some(fedimintd) = self.members.get_mut(&peer_id) else {
770 bail!("fedimintd-{peer_id} does not exist");
771 };
772 fedimintd.await_terminated().await?;
773 self.members.remove(&peer_id);
774 Ok(())
775 }
776
777 pub async fn start_all_servers(&mut self, process_mgr: &ProcessManager) -> Result<()> {
779 info!("starting all servers");
780 let fed_size = process_mgr.globals.FM_FED_SIZE;
781 for peer_id in 0..fed_size {
782 if self.members.contains_key(&peer_id) {
783 continue;
784 }
785 self.start_server(process_mgr, peer_id).await?;
786 }
787 self.await_all_peers().await?;
788 Ok(())
789 }
790
791 pub async fn terminate_all_servers(&mut self) -> Result<()> {
793 info!("terminating all servers");
794 let running_peer_ids: Vec<_> = self.members.keys().copied().collect();
795 for peer_id in running_peer_ids {
796 self.terminate_server(peer_id).await?;
797 }
798 Ok(())
799 }
800
801 pub async fn restart_all_staggered_with_bin(
806 &mut self,
807 process_mgr: &ProcessManager,
808 bin_path: &PathBuf,
809 ) -> Result<()> {
810 let fed_size = process_mgr.globals.FM_FED_SIZE;
811
812 self.start_all_servers(process_mgr).await?;
814
815 while self.num_members() > 0 {
817 self.terminate_server(self.num_members() - 1).await?;
818 if self.num_members() > 0 {
819 fedimint_core::task::sleep_in_test(
820 "waiting to shutdown remaining peers",
821 Duration::from_secs(10),
822 )
823 .await;
824 }
825 }
826
827 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", bin_path) };
829
830 for peer_id in 0..fed_size {
832 self.start_server(process_mgr, peer_id).await?;
833 if peer_id < fed_size - 1 {
834 fedimint_core::task::sleep_in_test(
835 "waiting to restart remaining peers",
836 Duration::from_secs(10),
837 )
838 .await;
839 }
840 }
841
842 self.await_all_peers().await?;
843
844 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
845 info!("upgraded fedimintd to version: {}", fedimintd_version);
846 Ok(())
847 }
848
849 pub async fn restart_all_with_bin(
850 &mut self,
851 process_mgr: &ProcessManager,
852 bin_path: &PathBuf,
853 ) -> Result<()> {
854 let current_fedimintd_path = std::env::var("FM_FEDIMINTD_BASE_EXECUTABLE")?;
856 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", bin_path) };
858 unsafe { std::env::set_var("FM_FEDIMINTD_BASE_EXECUTABLE", current_fedimintd_path) };
860
861 self.restart_all_staggered_with_bin(process_mgr, bin_path)
862 .await
863 }
864
865 pub async fn degrade_federation(&mut self, process_mgr: &ProcessManager) -> Result<()> {
866 let fed_size = process_mgr.globals.FM_FED_SIZE;
867 let offline_nodes = process_mgr.globals.FM_OFFLINE_NODES;
868 anyhow::ensure!(
869 fed_size > 3 * offline_nodes,
870 "too many offline nodes ({offline_nodes}) to reach consensus"
871 );
872
873 while self.num_members() > fed_size - offline_nodes {
874 self.terminate_server(self.num_members() - 1).await?;
875 }
876
877 if offline_nodes > 0 {
878 info!(fed_size, offline_nodes, "federation is degraded");
879 }
880 Ok(())
881 }
882
883 pub async fn send_to_address(&self, address: String, amount: u64) -> Result<()> {
884 self.bitcoind.send_to(address, amount).await?;
885
886 self.bitcoind.mine_blocks(21).await?;
887
888 Ok(())
889 }
890
891 pub async fn pegin_client_no_wait(&self, amount: u64, client: &Client) -> Result<String> {
892 let deposit_fees_msat = self.deposit_fees()?.msats;
893 assert_eq!(
894 deposit_fees_msat % 1000,
895 0,
896 "Deposit fees expected to be whole sats in test suite"
897 );
898 let deposit_fees = deposit_fees_msat / 1000;
899 info!(amount, deposit_fees, "Pegging-in client funds");
900
901 let (address, handle) = client.get_deposit_addr().await?;
904
905 self.bitcoind
906 .send_to(address, amount + deposit_fees)
907 .await?;
908 self.bitcoind.mine_blocks(21).await?;
909
910 Ok(handle)
911 }
912
913 pub async fn pegin_client(&self, amount: u64, client: &Client) -> Result<()> {
914 let walletv2_await_receive = crate::util::supports_wallet_v2()
915 && crate::util::FedimintCli::version_or_default().await >= *VERSION_0_12_0_ALPHA;
916
917 let initial_balance = if crate::util::supports_wallet_v2() && !walletv2_await_receive {
921 Some(client.balance().await?)
922 } else {
923 None
924 };
925
926 let handle = self.pegin_client_no_wait(amount, client).await?;
927
928 if walletv2_await_receive {
929 client.await_receive(&handle).await?;
932 } else if let Some(initial) = initial_balance {
933 let expected_balance = initial + (amount * 1000 * 9 / 10);
936 client.await_balance(expected_balance).await?;
937 } else {
938 client.await_deposit(&handle).await?;
939 }
940
941 Ok(())
942 }
943
944 pub async fn pegin_gateways(
947 &self,
948 amount: u64,
949 gateways: Vec<&super::gatewayd::Gatewayd>,
950 ) -> Result<()> {
951 let deposit_fees_msat = self.deposit_fees()?.msats;
952 assert_eq!(
953 deposit_fees_msat % 1000,
954 0,
955 "Deposit fees expected to be whole sats in test suite"
956 );
957 let deposit_fees = deposit_fees_msat / 1000;
958 info!(amount, deposit_fees, "Pegging-in gateway funds");
959 let fed_id = self.calculate_federation_id();
960
961 let uses_walletv2 = crate::util::supports_wallet_v2();
962
963 let walletv2_claim_wait = uses_walletv2
968 && crate::util::GatewayCli::version_or_default().await >= *VERSION_0_10_0_ALPHA;
969
970 let mut initial_balances = Vec::new();
974 if uses_walletv2 {
975 for gw in &gateways {
976 let balance = gw
977 .client()
978 .ecash_balance(fed_id.clone())
979 .await
980 .expect("failed to fetch initial gateway balance");
981 initial_balances.push(balance);
982 }
983 }
984
985 let mut gateway_deposit_addrs = Vec::new();
986 for gw in gateways.clone() {
987 let pegin_addr = gw.client().get_pegin_addr(&fed_id).await?;
988 debug!(
989 gateway = %gw.gw_name,
990 ln = %gw.ln.ln_type(),
991 address = %pegin_addr,
992 amount_sats = amount + deposit_fees,
993 uses_walletv2,
994 "Sending gateway pegin"
995 );
996 self.bitcoind
997 .send_to(pegin_addr.clone(), amount + deposit_fees)
998 .await?;
999 gateway_deposit_addrs.push(pegin_addr);
1000 }
1001
1002 let pegin_start = Instant::now();
1003 self.bitcoind.mine_blocks(21).await?;
1004 let bitcoind_block_height: u64 = self.bitcoind.get_block_count().await? - 1;
1005 debug!(
1006 bitcoind_block_height,
1007 elapsed_ms = %pegin_start.elapsed().as_millis(),
1008 "Mined gateway pegin blocks"
1009 );
1010
1011 if walletv2_claim_wait {
1012 const CLAIM_TIMEOUT: Duration = Duration::from_secs(180);
1017 const PAYMENT_LOG_PAGE_SIZE: usize = 100;
1018
1019 try_join_all(
1020 gateways
1021 .iter()
1022 .enumerate()
1023 .filter(|(_, gw)| gw.gatewayd_version >= *VERSION_0_11_0_ALPHA)
1024 .map(|(i, gw)| {
1025 let fed_id = fed_id.clone();
1026 let gateway_name = gw.gw_name.clone();
1027 let gateway_ln = gw.ln.ln_type().to_string();
1028 let deposit_address = gateway_deposit_addrs[i].clone();
1029 poll_with_timeout("gateway pegin claim", CLAIM_TIMEOUT, move || {
1030 let fed_id = fed_id.clone();
1031 let gateway_name = gateway_name.clone();
1032 let gateway_ln = gateway_ln.clone();
1033 let deposit_address = deposit_address.clone();
1034 async move {
1035 let events = gw
1036 .client()
1037 .payment_log(
1038 &fed_id,
1039 &["payment-receive", "payment-receive-update"],
1040 PAYMENT_LOG_PAGE_SIZE,
1041 )
1042 .await
1043 .map_err(ControlFlow::Continue)?;
1044
1045 if !walletv2_receive_claimed(&events, &deposit_address) {
1046 return Err(ControlFlow::Continue(anyhow::anyhow!(
1047 "Gateway {gateway_name} ({gateway_ln}) has not claimed the deposit to {deposit_address} yet"
1048 )));
1049 }
1050
1051 debug!(
1052 gateway = %gateway_name,
1053 ln = %gateway_ln,
1054 address = %deposit_address,
1055 elapsed_ms = %pegin_start.elapsed().as_millis(),
1056 "Walletv2 gateway pegin claimed"
1057 );
1058
1059 Ok(())
1060 }
1061 })
1062 }),
1063 )
1064 .await?;
1065 }
1066
1067 try_join_all(gateways.into_iter().enumerate().map(|(i, gw)| {
1068 let initial_balance = if uses_walletv2 {
1069 initial_balances[i]
1070 } else {
1071 0
1072 };
1073 let gateway_name = gw.gw_name.clone();
1074 let gateway_ln = gw.ln.ln_type().to_string();
1075 let gateway_id = gw.gateway_id.clone();
1076 let gateway_index = gw.gateway_index;
1077 let deposit_address = gateway_deposit_addrs[i].clone();
1078 let fed_id = fed_id.clone();
1079 poll("gateway pegin", move || {
1080 let fed_id = fed_id.clone();
1081 let gateway_name = gateway_name.clone();
1082 let gateway_ln = gateway_ln.clone();
1083 let gateway_id = gateway_id.clone();
1084 let deposit_address = deposit_address.clone();
1085 async move {
1086 let gw_info = gw
1087 .client()
1088 .get_info()
1089 .await
1090 .map_err(ControlFlow::Continue)?;
1091
1092 let block_height: u64 = if gw.gatewayd_version < *VERSION_0_10_0_ALPHA {
1093 gw_info["block_height"]
1094 .as_u64()
1095 .expect("Could not parse block height")
1096 } else {
1097 gw_info["lightning_info"]["connected"]["block_height"]
1098 .as_u64()
1099 .expect("Could not parse block height")
1100 };
1101
1102 if bitcoind_block_height != block_height {
1103 debug!(
1104 gateway = %gateway_name,
1105 ln = %gateway_ln,
1106 bitcoind_block_height,
1107 gateway_block_height = block_height,
1108 elapsed_ms = %pegin_start.elapsed().as_millis(),
1109 "Waiting for gateway pegin block sync"
1110 );
1111 return Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
1112 "Gateway {gateway_name} ({gateway_id}, index {gateway_index}) block height {block_height} has not reached bitcoind block height {bitcoind_block_height}"
1113 )));
1114 }
1115
1116 let gateway_balance = gw
1117 .client()
1118 .ecash_balance(fed_id)
1119 .await
1120 .map_err(ControlFlow::Continue)?;
1121
1122 if uses_walletv2 {
1123 let expected = initial_balance + (amount * 1000 * 9 / 10);
1127 debug!(
1128 gateway = %gateway_name,
1129 ln = %gateway_ln,
1130 gateway_balance_msats = gateway_balance,
1131 expected_msats = expected,
1132 initial_balance_msats = initial_balance,
1133 elapsed_ms = %pegin_start.elapsed().as_millis(),
1134 "Checked walletv2 gateway pegin balance"
1135 );
1136 if expected <= gateway_balance {
1137 Ok(())
1138 } else {
1139 Err(ControlFlow::Continue(anyhow::anyhow!(
1140 "Gateway {gateway_name} ({gateway_id}, index {gateway_index}) balance {gateway_balance} has not reached expected {expected} for deposit address {deposit_address} (initial: {initial_balance})"
1141 )))
1142 }
1143 } else {
1144 poll_almost_equal!(gateway_balance, amount * 1000)
1145 }
1146 }
1147 })
1148 }))
1149 .await?;
1150
1151 Ok(())
1152 }
1153
1154 pub async fn pegout_gateways(
1158 &self,
1159 amount: u64,
1160 gateways: Vec<&super::gatewayd::Gatewayd>,
1161 ) -> Result<()> {
1162 info!(amount, "Pegging-out gateway funds");
1163 let fed_id = self.calculate_federation_id();
1164 let mut peg_outs: BTreeMap<LightningNodeType, (Amount, WithdrawResponse)> = BTreeMap::new();
1165 for gw in gateways.clone() {
1166 let prev_fed_ecash_balance = gw
1167 .client()
1168 .get_balances()
1169 .await?
1170 .ecash_balances
1171 .into_iter()
1172 .find(|fed| fed.federation_id.to_string() == fed_id)
1173 .expect("Gateway has not joined federation")
1174 .ecash_balance_msats;
1175
1176 let pegout_address = self.bitcoind.get_new_address().await?;
1177 let response = gw
1178 .client()
1179 .pegout(fed_id.clone(), amount, pegout_address)
1180 .await?;
1181 peg_outs.insert(gw.ln.ln_type(), (prev_fed_ecash_balance, response));
1182 }
1183 try_join_all(
1187 peg_outs
1188 .values()
1189 .map(|(_, pegout)| self.bitcoind.poll_get_transaction(pegout.txid)),
1190 )
1191 .await?;
1192 self.finalize_mempool_tx().await?;
1193
1194 for gw in gateways.clone() {
1195 let after_fed_ecash_balance = gw
1196 .client()
1197 .get_balances()
1198 .await?
1199 .ecash_balances
1200 .into_iter()
1201 .find(|fed| fed.federation_id.to_string() == fed_id)
1202 .expect("Gateway has not joined federation")
1203 .ecash_balance_msats;
1204
1205 let ln_type = gw.ln.ln_type();
1206 let prev_balance = peg_outs
1207 .get(&ln_type)
1208 .expect("peg out does not exist")
1209 .0
1210 .msats;
1211 let fees = peg_outs
1212 .get(&ln_type)
1213 .expect("peg out does not exist")
1214 .1
1215 .fees;
1216 let total_fee = fees.amount().to_sat() * 1000;
1217 let tolerance = if crate::util::supports_wallet_v2() {
1220 let amount_sats = amount / 1000;
1221 let module_fee_sats = 100 + amount_sats / 100;
1222 module_fee_sats * 1000 + 2000
1223 } else if crate::util::supports_mint_v2() {
1224 4000
1225 } else {
1226 2000
1227 };
1228 crate::util::almost_equal(
1229 after_fed_ecash_balance.msats,
1230 prev_balance - amount - total_fee,
1231 tolerance,
1232 )
1233 .map_err(|e| {
1234 anyhow::anyhow!(
1235 "new balance did not equal prev balance minus withdraw_amount minus fees: {e}"
1236 )
1237 })?;
1238 }
1239
1240 Ok(())
1241 }
1242
1243 pub fn calculate_federation_id(&self) -> String {
1244 self.client_config()
1245 .unwrap()
1246 .global
1247 .calculate_federation_id()
1248 .to_string()
1249 }
1250
1251 pub async fn await_block_sync(&self) -> Result<u64> {
1252 let finality_delay = self.get_finality_delay()?;
1253 let block_count = self.bitcoind.get_block_count().await?;
1254 let expected = block_count.saturating_sub(finality_delay.into());
1255
1256 if crate::util::supports_wallet_v2() {
1257 let client = self.internal_client().await?;
1259 loop {
1260 let value = cmd!(client, "module", "walletv2", "info", "block-count")
1261 .out_json()
1262 .await?;
1263 let current: u64 = serde_json::from_value(value)?;
1264 if current >= expected {
1265 break;
1266 }
1267 fedimint_core::task::sleep_in_test(
1268 format!("Waiting for consensus block count to reach {expected}"),
1269 std::time::Duration::from_secs(1),
1270 )
1271 .await;
1272 }
1273 } else {
1274 cmd!(
1275 self.internal_client().await?,
1276 "dev",
1277 "wait-block-count",
1278 expected
1279 )
1280 .run()
1281 .await?;
1282 }
1283
1284 Ok(expected)
1285 }
1286
1287 fn get_finality_delay(&self) -> Result<u32, anyhow::Error> {
1288 if crate::util::supports_wallet_v2() {
1290 return Ok(fedimint_walletv2_server::BLOCK_COUNT_VOTE_DELAY as u32);
1291 }
1292
1293 let wallet_instance_id = self.module_instance_id_by_kind(&fedimint_wallet_client::KIND)?;
1294 let client_config = &self.client_config()?;
1295 let wallet_cfg = client_config
1296 .modules
1297 .get(&wallet_instance_id)
1298 .context("wallet module not found")?
1299 .clone()
1300 .redecode_raw(&ModuleDecoderRegistry::new([(
1301 wallet_instance_id,
1302 fedimint_wallet_client::KIND,
1303 fedimint_wallet_client::WalletModuleTypes::decoder(),
1304 )]))?;
1305 let wallet_cfg: &WalletClientConfig = wallet_cfg.cast()?;
1306
1307 let finality_delay = wallet_cfg.finality_delay;
1308 Ok(finality_delay)
1309 }
1310
1311 pub async fn await_gateways_registered(&self) -> Result<()> {
1312 if !crate::util::supports_lnv1() {
1318 return Ok(());
1319 }
1320
1321 let start_time = Instant::now();
1322 debug!(target: LOG_DEVIMINT, "Awaiting LN gateways registration");
1323
1324 poll("gateways registered", || async {
1325 let num_gateways = cmd!(
1326 self.internal_client()
1327 .await
1328 .map_err(ControlFlow::Continue)?,
1329 "list-gateways"
1330 )
1331 .out_json()
1332 .await
1333 .map_err(ControlFlow::Continue)?
1334 .as_array()
1335 .context("invalid output")
1336 .map_err(ControlFlow::Break)?
1337 .len();
1338
1339 let expected_gateways =
1342 if crate::util::Gatewayd::version_or_default().await < *VERSION_0_10_0_ALPHA {
1343 1
1344 } else {
1345 2
1346 };
1347
1348 poll_eq!(num_gateways, expected_gateways)
1349 })
1350 .await?;
1351 debug!(target: LOG_DEVIMINT,
1352 elapsed_ms = %start_time.elapsed().as_millis(),
1353 "Gateways registered");
1354 Ok(())
1355 }
1356
1357 pub async fn await_all_peers(&self) -> Result<()> {
1363 let (module_name, endpoint) = if crate::util::supports_wallet_v2() {
1364 ("walletv2", "consensus_block_count")
1365 } else {
1366 ("wallet", "block_count")
1367 };
1368 poll("Waiting for all peers to be online", || async {
1369 cmd!(
1370 self.internal_client()
1371 .await
1372 .map_err(ControlFlow::Continue)?,
1373 "dev",
1374 "api",
1375 "--module",
1376 module_name,
1377 endpoint
1378 )
1379 .run()
1380 .await
1381 .map_err(ControlFlow::Continue)?;
1382
1383 query_running_peers(&self.members, |peer_id| async move {
1384 cmd!(
1385 self.internal_client().await?,
1386 "dev",
1387 "api",
1388 "--peer-id",
1389 peer_id,
1390 "--module",
1391 module_name,
1392 endpoint
1393 )
1394 .run()
1395 .await
1396 })
1397 .await
1398 .map_err(ControlFlow::Continue)?;
1399
1400 Ok(())
1401 })
1402 .await
1403 }
1404
1405 pub async fn await_peer(&self, peer_id: usize) -> Result<()> {
1406 poll("Waiting for all peers to be online", || async {
1407 cmd!(
1408 self.internal_client()
1409 .await
1410 .map_err(ControlFlow::Continue)?,
1411 "dev",
1412 "api",
1413 "--peer-id",
1414 peer_id,
1415 "--module",
1416 "wallet",
1417 "block_count"
1418 )
1419 .run()
1420 .await
1421 .map_err(ControlFlow::Continue)?;
1422 Ok(())
1423 })
1424 .await
1425 }
1426
1427 pub async fn finalize_mempool_tx(&self) -> Result<()> {
1437 let finality_delay = self.get_finality_delay()?;
1438 let blocks_to_mine = finality_delay + 1;
1439 self.bitcoind.mine_blocks(blocks_to_mine.into()).await?;
1440 self.await_block_sync().await?;
1441 Ok(())
1442 }
1443
1444 pub async fn mine_then_wait_blocks_sync(&self, blocks: u64) -> Result<()> {
1445 self.bitcoind.mine_blocks(blocks).await?;
1446 self.await_block_sync().await?;
1447 Ok(())
1448 }
1449
1450 pub fn num_members(&self) -> usize {
1451 self.members.len()
1452 }
1453
1454 pub fn member_ids(&self) -> impl Iterator<Item = PeerId> + '_ {
1455 self.members
1456 .keys()
1457 .map(|&peer_id| PeerId::from(peer_id as u16))
1458 }
1459}
1460
1461fn walletv2_receive_claimed(events: &serde_json::Value, deposit_address: &str) -> bool {
1469 let Some(events) = events.as_array() else {
1470 return false;
1471 };
1472
1473 let is_walletv2 = |event: &serde_json::Value| event["module"]["kind"] == "walletv2";
1474
1475 let receive_operations = events
1476 .iter()
1477 .filter(|event| {
1478 is_walletv2(event)
1479 && event["kind"] == "payment-receive"
1480 && event["payload"]["address"] == deposit_address
1481 })
1482 .map(|event| &event["payload"]["operation_id"])
1483 .collect::<Vec<_>>();
1484
1485 events.iter().any(|event| {
1486 is_walletv2(event)
1487 && event["kind"] == "payment-receive-update"
1488 && event["payload"]["status"] == "Success"
1489 && receive_operations.contains(&&event["payload"]["operation_id"])
1490 })
1491}
1492
1493#[derive(Clone)]
1494pub struct Fedimintd {
1495 _bitcoind: Bitcoind,
1496 process: ProcessHandle,
1497}
1498
1499impl Fedimintd {
1500 pub async fn new(
1501 process_mgr: &ProcessManager,
1502 bitcoind: Bitcoind,
1503 peer_id: usize,
1504 env: &vars::Fedimintd,
1505 fed_name: String,
1506 ) -> Result<Self> {
1507 debug!(target: LOG_DEVIMINT, "Starting fedimintd-{fed_name}-{peer_id}");
1508 let process = process_mgr
1509 .spawn_daemon(
1510 &format!("fedimintd-{fed_name}-{peer_id}"),
1511 cmd!(FedimintdCmd).envs(env.vars()),
1512 )
1513 .await?;
1514
1515 Ok(Self {
1516 _bitcoind: bitcoind,
1517 process,
1518 })
1519 }
1520
1521 pub async fn terminate(self) -> Result<()> {
1522 self.process.terminate().await
1523 }
1524
1525 pub async fn await_terminated(&self) -> Result<()> {
1526 self.process.await_terminated().await
1527 }
1528}
1529
1530pub async fn run_cli_dkg_v2(endpoints: BTreeMap<PeerId, String>) -> Result<()> {
1531 let status_futures = endpoints.values().map(|endpoint| {
1533 let endpoint = endpoint.clone();
1534 async move {
1535 let status = poll("awaiting-setup-status-awaiting-local-params", || async {
1536 crate::util::FedimintCli
1537 .setup_status(&API_AUTH, &endpoint)
1538 .await
1539 .map_err(ControlFlow::Continue)
1540 })
1541 .await
1542 .unwrap();
1543
1544 assert_eq!(status, SetupStatus::AwaitingLocalParams);
1545 }
1546 });
1547 join_all(status_futures).await;
1548
1549 debug!(target: LOG_DEVIMINT, "Setting local parameters...");
1550
1551 let federation_size =
1554 if crate::util::FedimintCli::version_or_default().await >= *VERSION_0_11_0_ALPHA {
1555 Some(endpoints.len())
1556 } else {
1557 None
1558 };
1559 let local_params_futures = endpoints.iter().map(|(peer, endpoint)| {
1560 let peer = *peer;
1561 let endpoint = endpoint.clone();
1562 async move {
1563 let info = if peer.to_usize() == 0 {
1564 crate::util::FedimintCli
1565 .set_local_params_leader(&peer, &API_AUTH, &endpoint, federation_size)
1566 .await
1567 } else {
1568 crate::util::FedimintCli
1569 .set_local_params_follower(&peer, &API_AUTH, &endpoint)
1570 .await
1571 };
1572 info.map(|i| (peer, i))
1573 }
1574 });
1575 let connection_info: BTreeMap<_, _> = try_join_all(local_params_futures)
1576 .await?
1577 .into_iter()
1578 .collect();
1579
1580 debug!(target: LOG_DEVIMINT, "Exchanging peer connection info...");
1581
1582 let add_peer_futures = connection_info.iter().flat_map(|(peer, info)| {
1585 endpoints
1586 .iter()
1587 .filter(move |(p, _)| *p != peer)
1588 .map(move |(_, endpoint)| {
1589 let endpoint = endpoint.clone();
1590 let info = info.clone();
1591 async move {
1592 crate::util::FedimintCli
1593 .add_peer(&info, &API_AUTH, &endpoint)
1594 .await
1595 }
1596 })
1597 });
1598 try_join_all(add_peer_futures).await?;
1599
1600 debug!(target: LOG_DEVIMINT, "Starting DKG...");
1601
1602 let start_dkg_futures = endpoints.values().map(|endpoint| {
1604 let endpoint = endpoint.clone();
1605 async move {
1606 crate::util::FedimintCli
1607 .start_dkg(&API_AUTH, &endpoint)
1608 .await
1609 }
1610 });
1611 try_join_all(start_dkg_futures).await?;
1612
1613 Ok(())
1614}
1615
1616#[cfg(test)]
1617mod tests;
1618
1619#[cfg(test)]
1620mod client_config_tests;