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