1use std::collections::BTreeMap;
2use std::ffi::OsStr;
3use std::future::Future;
4use std::ops::ControlFlow;
5use std::path::PathBuf;
6use std::process::Stdio;
7use std::sync::Arc;
8use std::time::Duration;
9use std::{env, unreachable};
10
11use anyhow::{Context, Result, anyhow, bail, format_err};
12use fedimint_api_client::api::StatusResponse;
13use fedimint_core::PeerId;
14use fedimint_core::admin_client::{PeerServerParamsLegacy, SetupStatus};
15use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
16use fedimint_core::envs::{FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set};
17use fedimint_core::module::ApiAuth;
18use fedimint_core::task::{self, block_in_place, block_on};
19use fedimint_core::time::now;
20use fedimint_core::util::FmtCompactAnyhow as _;
21use fedimint_core::util::backoff_util::custom_backoff;
22use fedimint_logging::LOG_DEVIMINT;
23use legacy_types::ConfigGenParamsResponseLegacy;
24use semver::Version;
25use serde::de::DeserializeOwned;
26use tokio::fs::OpenOptions;
27use tokio::process::Child;
28use tokio::sync::Mutex;
29use tracing::{debug, warn};
30
31use crate::envs::{
32 FM_BACKWARDS_COMPATIBILITY_TEST_ENV, FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV,
33 FM_BITCOIND_BASE_EXECUTABLE_ENV, FM_BTC_CLIENT_ENV, FM_CLIENT_DIR_ENV,
34 FM_DEVIMINT_CMD_INHERIT_STDERR_ENV, FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV,
35 FM_ELECTRS_BASE_EXECUTABLE_ENV, FM_ESPLORA_BASE_EXECUTABLE_ENV,
36 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV,
37 FM_FEDIMINTD_BASE_EXECUTABLE_ENV, FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV,
38 FM_GATEWAYD_BASE_EXECUTABLE_ENV, FM_GWCLI_LDK_ENV, FM_GWCLI_LND_ENV,
39 FM_LNCLI_BASE_EXECUTABLE_ENV, FM_LNCLI_ENV, FM_LND_BASE_EXECUTABLE_ENV,
40 FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV, FM_LOGS_DIR_ENV, FM_MINT_CLIENT_ENV,
41 FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV, FM_RECURRINGD_BASE_EXECUTABLE_ENV,
42};
43
44const DEFAULT_VERSION: Version = Version::new(0, 2, 1);
47
48pub fn parse_map(s: &str) -> Result<BTreeMap<String, String>> {
49 let mut map = BTreeMap::new();
50
51 if s.is_empty() {
52 return Ok(map);
53 }
54
55 for pair in s.split(',') {
56 let parts: Vec<&str> = pair.split('=').collect();
57 if parts.len() == 2 {
58 map.insert(parts[0].to_string(), parts[1].to_string());
59 } else {
60 return Err(format_err!("Invalid pair in map: {}", pair));
61 }
62 }
63 Ok(map)
64}
65
66fn send_sigterm(child: &Child) {
67 send_signal(child, nix::sys::signal::Signal::SIGTERM);
68}
69
70fn send_sigkill(child: &Child) {
71 send_signal(child, nix::sys::signal::Signal::SIGKILL);
72}
73
74fn send_signal(child: &Child, signal: nix::sys::signal::Signal) {
75 let _ = nix::sys::signal::kill(
76 nix::unistd::Pid::from_raw(child.id().expect("pid should be present") as _),
77 signal,
78 );
79}
80
81#[derive(Debug, Clone)]
86pub struct ProcessHandle(Arc<Mutex<ProcessHandleInner>>);
87
88impl ProcessHandle {
89 pub async fn terminate(&self) -> Result<()> {
90 let mut inner = self.0.lock().await;
91 inner.terminate().await?;
92 Ok(())
93 }
94 pub async fn is_running(&self) -> bool {
95 self.0.lock().await.child.is_some()
96 }
97}
98
99#[derive(Debug)]
100pub struct ProcessHandleInner {
101 name: String,
102 child: Option<Child>,
103}
104
105impl ProcessHandleInner {
106 async fn terminate(&mut self) -> anyhow::Result<()> {
107 if let Some(child) = self.child.as_mut() {
108 debug!(
109 target: LOG_DEVIMINT,
110 name=%self.name,
111 signal="SIGTERM",
112 "sending signal to terminate child process"
113 );
114
115 send_sigterm(child);
116
117 if (fedimint_core::runtime::timeout(Duration::from_secs(2), child.wait()).await)
118 .is_err()
119 {
120 debug!(
121 target: LOG_DEVIMINT,
122 name=%self.name,
123 signal="SIGKILL",
124 "sending signal to terminate child process"
125 );
126
127 send_sigkill(child);
128
129 match fedimint_core::runtime::timeout(Duration::from_secs(5), child.wait()).await {
130 Ok(Ok(_)) => {}
131 Ok(Err(err)) => {
132 bail!("Failed to terminate child process {}: {}", self.name, err);
133 }
134 Err(_) => {
135 bail!("Failed to terminate child process {}: timeout", self.name);
136 }
137 }
138 }
139 }
140 self.child.take();
142 Ok(())
143 }
144}
145
146impl Drop for ProcessHandleInner {
147 fn drop(&mut self) {
148 if self.child.is_none() {
149 return;
150 }
151
152 block_in_place(|| {
153 if let Err(err) = block_on(self.terminate()) {
154 warn!(target: LOG_DEVIMINT,
155 name=%self.name,
156 err = %err.fmt_compact_anyhow(),
157 "Error terminating process on drop");
158 }
159 });
160 }
161}
162
163#[derive(Clone)]
164pub struct ProcessManager {
165 pub globals: super::vars::Global,
166}
167
168impl ProcessManager {
169 pub fn new(globals: super::vars::Global) -> Self {
170 Self { globals }
171 }
172
173 pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
175 debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
176 let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
177 let path = format!("{logs_dir}/{name}.log");
178 let log = OpenOptions::new()
179 .append(true)
180 .create(true)
181 .open(path)
182 .await?
183 .into_std()
184 .await;
185 cmd.cmd.kill_on_drop(false); cmd.cmd.stdout(log.try_clone()?);
187 cmd.cmd.stderr(log);
188 let child = cmd
189 .cmd
190 .spawn()
191 .with_context(|| format!("Could not spawn: {name}"))?;
192 let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
193 name: name.to_owned(),
194 child: Some(child),
195 })));
196 Ok(handle)
197 }
198}
199
200pub struct Command {
201 pub cmd: tokio::process::Command,
202 pub args_debug: Vec<String>,
203}
204
205impl Command {
206 pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
207 let string = arg.to_string();
208 self.cmd.arg(string.clone());
209 self.args_debug.push(string);
210 self
211 }
212
213 pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
214 for arg in args {
215 self = self.arg(&arg);
216 }
217 self
218 }
219
220 pub fn env<K, V>(mut self, key: K, val: V) -> Self
221 where
222 K: AsRef<OsStr>,
223 V: AsRef<OsStr>,
224 {
225 self.cmd.env(key, val);
226 self
227 }
228
229 pub fn envs<I, K, V>(mut self, env: I) -> Self
230 where
231 I: IntoIterator<Item = (K, V)>,
232 K: AsRef<OsStr>,
233 V: AsRef<OsStr>,
234 {
235 self.cmd.envs(env);
236 self
237 }
238
239 pub fn kill_on_drop(mut self, kill: bool) -> Self {
240 self.cmd.kill_on_drop(kill);
241 self
242 }
243
244 pub async fn out_json(&mut self) -> Result<serde_json::Value> {
246 Ok(serde_json::from_str(&self.out_string().await?)?)
247 }
248
249 fn command_debug(&self) -> String {
250 self.args_debug
251 .iter()
252 .map(|x| x.replace(' ', "␣"))
253 .collect::<Vec<_>>()
254 .join(" ")
255 }
256
257 pub async fn out_string(&mut self) -> Result<String> {
259 let output = self
260 .run_inner(true)
261 .await
262 .with_context(|| format!("command: {}", self.command_debug()))?;
263 let output = String::from_utf8(output.stdout)?;
264 Ok(output.trim().to_owned())
265 }
266
267 pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
269 let output = self
270 .run_inner(false)
271 .await
272 .with_context(|| format!("command: {}", self.command_debug()))?;
273 let output = String::from_utf8(output.stdout)?;
274 Ok(serde_json::from_str(output.trim())?)
275 }
276
277 pub async fn assert_error(
280 &mut self,
281 predicate: impl Fn(serde_json::Value) -> bool,
282 ) -> Result<()> {
283 let parsed_error = self.expect_err_json().await?;
284 anyhow::ensure!(predicate(parsed_error));
285 Ok(())
286 }
287
288 pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
291 self.assert_error(|err_json| {
292 let error_string = err_json
293 .get("error")
294 .expect("json error contains error field")
295 .as_str()
296 .expect("not a string")
297 .to_owned();
298
299 error_string.contains(error)
300 })
301 .await
302 }
303
304 pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
305 debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
306 let output = self
307 .cmd
308 .stdout(Stdio::piped())
309 .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
310 Stdio::inherit()
311 } else {
312 Stdio::piped()
313 })
314 .spawn()?
315 .wait_with_output()
316 .await?;
317
318 if output.status.success() != expect_success {
319 bail!(
320 "{}\nstdout:\n{}\nstderr:\n{}\n",
321 output.status,
322 String::from_utf8_lossy(&output.stdout),
323 String::from_utf8_lossy(&output.stderr),
324 );
325 }
326 Ok(output)
327 }
328
329 pub async fn run(&mut self) -> Result<()> {
331 let _ = self
332 .run_inner(true)
333 .await
334 .with_context(|| format!("command: {}", self.command_debug()))?;
335 Ok(())
336 }
337
338 pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
340 let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
341 let path = format!("{logs_dir}/{name}.log");
342 let log = OpenOptions::new()
343 .append(true)
344 .create(true)
345 .open(&path)
346 .await
347 .with_context(|| format!("path: {path} cmd: {name}"))?
348 .into_std()
349 .await;
350 self.cmd.stdout(log.try_clone()?);
351 self.cmd.stderr(log);
352 let status = self
353 .cmd
354 .spawn()
355 .with_context(|| format!("cmd: {name}"))?
356 .wait()
357 .await?;
358 if !status.success() {
359 bail!("{}", status);
360 }
361 Ok(())
362 }
363}
364
365#[macro_export]
377macro_rules! cmd {
378 ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
379 cmd! {
380 @head ($($($head)*)? format!($curr),)
381 $($($tail)*)?
382 }
383 };
384 ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
385 cmd! {
386 @head ($($($head)*)? $curr,)
387 $($($tail)*)?
388 }
389 };
390 (@head ($($head:tt)* )) => {
391 cmd! {
392 @last
393 $($head)*
394 }
395 };
396 (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
398 {
399 #[allow(unused)]
400 use $crate::util::ToCmdExt;
401 $this.cmd()
402 $(.arg(&$arg))*
403 .kill_on_drop(true)
404 .env("RUST_BACKTRACE", "1")
405 }
406 };
407}
408
409#[macro_export]
410macro_rules! poll_eq {
411 ($left:expr_2021, $right:expr_2021) => {
412 match ($left, $right) {
413 (left, right) => {
414 if left == right {
415 Ok(())
416 } else {
417 Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
418 "assertion failed, left: {left:?} right: {right:?}"
419 )))
420 }
421 }
422 }
423 };
424}
425
426pub(crate) use cmd;
428
429pub async fn poll_with_timeout<Fut, R>(
436 name: &str,
437 timeout: Duration,
438 f: impl Fn() -> Fut,
439) -> Result<R>
440where
441 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
442{
443 const MIN_BACKOFF: Duration = Duration::from_millis(50);
444 const MAX_BACKOFF: Duration = Duration::from_secs(1);
445
446 let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
447 let start = now();
448 for attempt in 0u64.. {
449 let attempt_start = now();
450 match f().await {
451 Ok(value) => return Ok(value),
452 Err(ControlFlow::Break(err)) => {
453 return Err(err).with_context(|| format!("polling {name}"));
454 }
455 Err(ControlFlow::Continue(err))
456 if attempt_start
457 .duration_since(start)
458 .expect("time goes forward")
459 < timeout =>
460 {
461 debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
462 task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
463 }
464 Err(ControlFlow::Continue(err)) => {
465 return Err(err).with_context(|| {
466 format!(
467 "Polling {name} failed after {attempt} retries (timeout: {}s)",
468 timeout.as_secs()
469 )
470 });
471 }
472 }
473 }
474
475 unreachable!();
476}
477
478const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(60);
479
480pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
487where
488 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
489{
490 poll_with_timeout(name, DEFAULT_POLL_TIMEOUT, f).await
491}
492
493pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
494where
495 Fut: Future<Output = Result<R, anyhow::Error>>,
496{
497 poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
498}
499
500pub trait ToCmdExt {
502 fn cmd(self) -> Command;
503}
504
505impl ToCmdExt for &'_ str {
507 fn cmd(self) -> Command {
508 Command {
509 cmd: tokio::process::Command::new(self),
510 args_debug: vec![self.to_owned()],
511 }
512 }
513}
514
515impl ToCmdExt for Vec<String> {
516 fn cmd(self) -> Command {
517 to_command(self)
518 }
519}
520
521pub trait JsonValueExt {
522 fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
523}
524
525impl JsonValueExt for serde_json::Value {
526 fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
527 Ok(serde_json::from_value(self)?)
528 }
529}
530
531const GATEWAYD_FALLBACK: &str = "gatewayd";
532
533const FEDIMINTD_FALLBACK: &str = "fedimintd";
534
535const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
536
537pub fn get_fedimint_cli_path() -> Vec<String> {
538 get_command_str_for_alias(
539 &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
540 &[FEDIMINT_CLI_FALLBACK],
541 )
542}
543
544const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
545
546pub fn get_gateway_cli_path() -> Vec<String> {
547 get_command_str_for_alias(
548 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
549 &[GATEWAY_CLI_FALLBACK],
550 )
551}
552
553const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
554
555const LNCLI_FALLBACK: &str = "lncli";
556
557pub fn get_lncli_path() -> Vec<String> {
558 get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
559}
560
561const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
562
563pub fn get_bitcoin_cli_path() -> Vec<String> {
564 get_command_str_for_alias(
565 &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
566 &[BITCOIN_CLI_FALLBACK],
567 )
568}
569
570const BITCOIND_FALLBACK: &str = "bitcoind";
571
572const LND_FALLBACK: &str = "lnd";
573
574const ELECTRS_FALLBACK: &str = "electrs";
575
576const ESPLORA_FALLBACK: &str = "esplora";
577
578const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
579
580const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
581
582const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
583
584pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
585 get_command_str_for_alias(
586 &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
587 &[FEDIMINT_DBTOOL_FALLBACK],
588 )
589}
590
591fn version_hash_to_version(version_hash: &str) -> Result<Version> {
593 match version_hash {
594 "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
595 "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
596 _ => Err(anyhow!("no version known for version hash: {version_hash}")),
597 }
598}
599
600pub struct FedimintdCmd;
601impl FedimintdCmd {
602 pub fn cmd(self) -> Command {
603 to_command(get_command_str_for_alias(
604 &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
605 &[FEDIMINTD_FALLBACK],
606 ))
607 }
608
609 pub async fn version_or_default() -> Version {
611 match cmd!(FedimintdCmd, "--version").out_string().await {
612 Ok(version) => parse_clap_version(&version),
613 Err(_) => cmd!(FedimintdCmd, "version-hash")
614 .out_string()
615 .await
616 .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
617 .unwrap_or(DEFAULT_VERSION),
618 }
619 }
620}
621
622pub struct Gatewayd;
623impl Gatewayd {
624 pub fn cmd(self) -> Command {
625 to_command(get_command_str_for_alias(
626 &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
627 &[GATEWAYD_FALLBACK],
628 ))
629 }
630
631 pub async fn version_or_default() -> Version {
633 match cmd!(Gatewayd, "--version").out_string().await {
634 Ok(version) => parse_clap_version(&version),
635 Err(_) => cmd!(Gatewayd, "version-hash")
636 .out_string()
637 .await
638 .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
639 .unwrap_or(DEFAULT_VERSION),
640 }
641 }
642}
643
644pub struct FedimintCli;
645impl FedimintCli {
646 pub fn cmd(self) -> Command {
647 to_command(get_command_str_for_alias(
648 &[FM_MINT_CLIENT_ENV],
649 &get_fedimint_cli_path()
650 .iter()
651 .map(String::as_str)
652 .collect::<Vec<_>>(),
653 ))
654 }
655
656 pub async fn version_or_default() -> Version {
658 match cmd!(FedimintCli, "--version").out_string().await {
659 Ok(version) => parse_clap_version(&version),
660 Err(_) => DEFAULT_VERSION,
661 }
662 }
663
664 pub async fn ws_status(self, endpoint: &str) -> Result<StatusResponse> {
665 let status = cmd!(self, "admin", "dkg", "--ws", endpoint, "ws-status")
666 .out_json()
667 .await?;
668 Ok(serde_json::from_value(status)?)
669 }
670
671 pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
672 cmd!(
673 self,
674 "--password",
675 &auth.0,
676 "admin",
677 "dkg",
678 "--ws",
679 endpoint,
680 "set-password",
681 )
682 .run()
683 .await
684 }
685
686 pub async fn set_local_params_leader(
687 self,
688 peer: &PeerId,
689 auth: &ApiAuth,
690 endpoint: &str,
691 ) -> Result<String> {
692 let json = cmd!(
693 self,
694 "--password",
695 &auth.0,
696 "admin",
697 "setup",
698 endpoint,
699 "set-local-params",
700 format!("Devimint Guardian {peer}"),
701 "--federation-name",
702 "Devimint Federation"
703 )
704 .out_json()
705 .await?;
706
707 Ok(serde_json::from_value(json)?)
708 }
709
710 pub async fn set_local_params_follower(
711 self,
712 peer: &PeerId,
713 auth: &ApiAuth,
714 endpoint: &str,
715 ) -> Result<String> {
716 let json = cmd!(
717 self,
718 "--password",
719 &auth.0,
720 "admin",
721 "setup",
722 endpoint,
723 "set-local-params",
724 format!("Devimint Guardian {peer}")
725 )
726 .out_json()
727 .await?;
728
729 Ok(serde_json::from_value(json)?)
730 }
731
732 pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
733 cmd!(
734 self,
735 "--password",
736 &auth.0,
737 "admin",
738 "setup",
739 endpoint,
740 "add-peer",
741 params
742 )
743 .run()
744 .await
745 }
746
747 pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
748 let json = cmd!(
749 self,
750 "--password",
751 &auth.0,
752 "admin",
753 "setup",
754 endpoint,
755 "status",
756 )
757 .out_json()
758 .await?;
759
760 Ok(serde_json::from_value(json)?)
761 }
762
763 pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
764 cmd!(
765 self,
766 "--password",
767 &auth.0,
768 "admin",
769 "setup",
770 endpoint,
771 "start-dkg"
772 )
773 .run()
774 .await
775 }
776
777 pub async fn set_config_gen_params(
778 self,
779 auth: &ApiAuth,
780 endpoint: &str,
781 meta: BTreeMap<String, String>,
782 server_gen_params: ServerModuleConfigGenParamsRegistry,
783 ) -> Result<()> {
784 cmd!(
785 self,
786 "--password",
787 &auth.0,
788 "admin",
789 "dkg",
790 "--ws",
791 endpoint,
792 "set-config-gen-params",
793 "--meta-json",
794 serde_json::to_string(&meta)?,
795 "--modules-json",
796 serde_json::to_string(&server_gen_params)?
797 )
798 .run()
799 .await
800 }
801
802 pub async fn consensus_config_gen_params_legacy(
803 self,
804 endpoint: &str,
805 ) -> Result<ConfigGenParamsResponseLegacy> {
806 let result = cmd!(
807 self,
808 "admin",
809 "dkg",
810 "--ws",
811 endpoint,
812 "consensus-config-gen-params"
813 )
814 .out_json()
815 .await
816 .context("non-json returned for consensus_config_gen_params")?;
817 Ok(serde_json::from_value(result)?)
818 }
819
820 pub async fn set_config_gen_connections(
821 self,
822 auth: &ApiAuth,
823 endpoint: &str,
824 our_name: &str,
825 leader_api_url: Option<&str>,
826 ) -> Result<()> {
827 if let Some(leader_api_url) = leader_api_url {
829 cmd!(
830 self,
831 "--password",
832 &auth.0,
833 "admin",
834 "dkg",
835 "--ws",
836 endpoint,
837 "set-config-gen-connections",
838 "--our-name",
839 our_name,
840 "--leader-api-url",
841 leader_api_url,
842 )
843 .run()
844 .await
845 } else {
846 cmd!(
847 self,
848 "--password",
849 &auth.0,
850 "admin",
851 "dkg",
852 "--ws",
853 endpoint,
854 "set-config-gen-connections",
855 "--our-name",
856 our_name,
857 )
858 .run()
859 .await
860 }
861 }
862
863 pub async fn get_config_gen_peers(self, endpoint: &str) -> Result<Vec<PeerServerParamsLegacy>> {
864 let result = cmd!(
865 self,
866 "admin",
867 "dkg",
868 "--ws",
869 endpoint,
870 "get-config-gen-peers"
871 )
872 .out_json()
873 .await
874 .context("non-json returned for get_config_gen_peers")?;
875 Ok(serde_json::from_value(result)?)
876 }
877
878 pub async fn run_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
879 cmd!(
880 self,
881 "--password",
882 &auth.0,
883 "admin",
884 "dkg",
885 "--ws",
886 endpoint,
887 "run-dkg"
888 )
889 .run()
890 .await
891 }
892
893 pub async fn get_verify_config_hash(
894 self,
895 auth: &ApiAuth,
896 endpoint: &str,
897 ) -> Result<BTreeMap<PeerId, bitcoincore_rpc::bitcoin::hashes::sha256::Hash>> {
898 let result = cmd!(
899 self,
900 "--password",
901 &auth.0,
902 "admin",
903 "dkg",
904 "--ws",
905 endpoint,
906 "get-verify-config-hash"
907 )
908 .out_json()
909 .await
910 .context("non-json returned for get_verify_config_hash")?;
911 Ok(serde_json::from_value(result)?)
912 }
913
914 pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
915 cmd!(
916 self,
917 "--password",
918 &auth.0,
919 "--our-id",
920 our_id,
921 "admin",
922 "shutdown",
923 session_count,
924 )
925 .run()
926 .await
927 }
928
929 pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
930 cmd!(
931 self,
932 "--password",
933 &auth.0,
934 "--our-id",
935 our_id,
936 "admin",
937 "status",
938 )
939 .run()
940 .await
941 }
942
943 pub async fn start_consensus(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
944 cmd!(
945 self,
946 "--password",
947 &auth.0,
948 "admin",
949 "dkg",
950 "--ws",
951 endpoint,
952 "start-consensus"
953 )
954 .run()
955 .await
956 }
957}
958
959pub struct LoadTestTool;
960impl LoadTestTool {
961 pub fn cmd(self) -> Command {
962 to_command(get_command_str_for_alias(
963 &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
964 &[LOAD_TEST_TOOL_FALLBACK],
965 ))
966 }
967}
968
969pub struct GatewayCli;
970impl GatewayCli {
971 pub fn cmd(self) -> Command {
972 to_command(get_command_str_for_alias(
973 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
974 &get_gateway_cli_path()
975 .iter()
976 .map(String::as_str)
977 .collect::<Vec<_>>(),
978 ))
979 }
980
981 pub async fn version_or_default() -> Version {
983 match cmd!(GatewayCli, "--version").out_string().await {
984 Ok(version) => parse_clap_version(&version),
985 Err(_) => DEFAULT_VERSION,
986 }
987 }
988}
989
990pub struct GatewayLndCli;
991impl GatewayLndCli {
992 pub fn cmd(self) -> Command {
993 to_command(get_command_str_for_alias(
994 &[FM_GWCLI_LND_ENV],
995 &["gateway-lnd"],
996 ))
997 }
998}
999
1000pub struct GatewayLdkCli;
1001impl GatewayLdkCli {
1002 pub fn cmd(self) -> Command {
1003 to_command(get_command_str_for_alias(
1004 &[FM_GWCLI_LDK_ENV],
1005 &["gateway-ldk"],
1006 ))
1007 }
1008}
1009
1010pub struct LnCli;
1011impl LnCli {
1012 pub fn cmd(self) -> Command {
1013 to_command(get_command_str_for_alias(
1014 &[FM_LNCLI_ENV],
1015 &get_lncli_path()
1016 .iter()
1017 .map(String::as_str)
1018 .collect::<Vec<_>>(),
1019 ))
1020 }
1021}
1022
1023pub struct BitcoinCli;
1024impl BitcoinCli {
1025 pub fn cmd(self) -> Command {
1026 to_command(get_command_str_for_alias(
1027 &[FM_BTC_CLIENT_ENV],
1028 &get_bitcoin_cli_path()
1029 .iter()
1030 .map(String::as_str)
1031 .collect::<Vec<_>>(),
1032 ))
1033 }
1034}
1035
1036pub struct Bitcoind;
1037impl Bitcoind {
1038 pub fn cmd(self) -> Command {
1039 to_command(get_command_str_for_alias(
1040 &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
1041 &[BITCOIND_FALLBACK],
1042 ))
1043 }
1044}
1045
1046pub struct Lnd;
1047impl Lnd {
1048 pub fn cmd(self) -> Command {
1049 to_command(get_command_str_for_alias(
1050 &[FM_LND_BASE_EXECUTABLE_ENV],
1051 &[LND_FALLBACK],
1052 ))
1053 }
1054}
1055
1056pub struct Electrs;
1057impl Electrs {
1058 pub fn cmd(self) -> Command {
1059 to_command(get_command_str_for_alias(
1060 &[FM_ELECTRS_BASE_EXECUTABLE_ENV],
1061 &[ELECTRS_FALLBACK],
1062 ))
1063 }
1064}
1065
1066pub struct Esplora;
1067impl Esplora {
1068 pub fn cmd(self) -> Command {
1069 to_command(get_command_str_for_alias(
1070 &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
1071 &[ESPLORA_FALLBACK],
1072 ))
1073 }
1074}
1075
1076pub struct Recoverytool;
1077impl Recoverytool {
1078 pub fn cmd(self) -> Command {
1079 to_command(get_command_str_for_alias(
1080 &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
1081 &[RECOVERYTOOL_FALLBACK],
1082 ))
1083 }
1084}
1085
1086pub struct DevimintFaucet;
1087impl DevimintFaucet {
1088 pub fn cmd(self) -> Command {
1089 to_command(get_command_str_for_alias(
1090 &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
1091 &[DEVIMINT_FAUCET_FALLBACK],
1092 ))
1093 }
1094}
1095
1096pub struct Recurringd;
1097impl Recurringd {
1098 pub fn cmd(self) -> Command {
1099 to_command(get_command_str_for_alias(
1100 &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
1101 &["fedimint-recurringd"],
1102 ))
1103 }
1104}
1105
1106fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
1107 for alias in aliases {
1109 if let Ok(cmd) = std::env::var(alias) {
1110 return cmd.split_whitespace().map(ToOwned::to_owned).collect();
1111 }
1112 }
1113 default.iter().map(ToString::to_string).collect()
1115}
1116
1117fn to_command(cli: Vec<String>) -> Command {
1118 let mut cmd = tokio::process::Command::new(&cli[0]);
1119 cmd.args(&cli[1..]);
1120 Command {
1121 cmd,
1122 args_debug: cli,
1123 }
1124}
1125
1126pub fn supports_lnv2() -> bool {
1127 std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
1128 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1129}
1130
1131pub fn is_backwards_compatibility_test() -> bool {
1133 is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1134}
1135
1136pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1140 let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1141 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1142 let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1143
1144 if pkg_version == fedimintd_version {
1145 unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1150 } else {
1151 let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1152
1153 let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1155 let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1156 unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1158 }
1159
1160 let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1161 let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1162 let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1163
1164 let fm_mint_client: String = format!(
1165 "{fedimint_cli} --data-dir {datadir}",
1166 fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1167 datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1168 );
1169 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1171
1172 Ok((original_fedimint_cli_path, original_fm_mint_client))
1173}
1174
1175pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1177 unsafe {
1179 std::env::set_var(
1180 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1181 original_fedimint_cli_path,
1182 );
1183 };
1184
1185 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1187}
1188
1189fn parse_clap_version(res: &str) -> Version {
1192 match res.split(' ').collect::<Vec<&str>>().as_slice() {
1193 [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1194 _ => DEFAULT_VERSION,
1195 }
1196}
1197
1198#[test]
1199fn test_parse_clap_version() -> Result<()> {
1200 let version_str = "fedimintd 0.3.0-alpha";
1201 let expected_version = Version::parse("0.3.0-alpha")?;
1202 assert_eq!(expected_version, parse_clap_version(version_str));
1203
1204 let version_str = "fedimintd 0.3.12";
1205 let expected_version = Version::parse("0.3.12")?;
1206 assert_eq!(expected_version, parse_clap_version(version_str));
1207
1208 let version_str = "fedimint-cli 2.12.2-rc22";
1209 let expected_version = Version::parse("2.12.2-rc22")?;
1210 assert_eq!(expected_version, parse_clap_version(version_str));
1211
1212 let version_str = "bad version";
1213 let expected_version = DEFAULT_VERSION;
1214 assert_eq!(expected_version, parse_clap_version(version_str));
1215
1216 Ok(())
1217}
1218
1219mod legacy_types {
1220 use std::collections::BTreeMap;
1221
1222 use fedimint_core::PeerId;
1223 use fedimint_core::admin_client::PeerServerParamsLegacy;
1224 use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
1225 use serde::{Deserialize, Serialize};
1226
1227 #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1230 pub struct ConfigGenParamsConsensusLegacy {
1231 pub peers: BTreeMap<PeerId, PeerServerParamsLegacy>,
1233 pub meta: BTreeMap<String, String>,
1235 pub modules: ServerModuleConfigGenParamsRegistry,
1237 }
1238
1239 #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1241 pub struct ConfigGenParamsResponseLegacy {
1242 pub consensus: ConfigGenParamsConsensusLegacy,
1244 pub our_current_id: PeerId,
1246 }
1247}