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_core::PeerId;
13use fedimint_core::admin_client::SetupStatus;
14use fedimint_core::envs::{
15 FM_ENABLE_MODULE_LNV2_ENV, FM_ENABLE_MODULE_MINTV2_ENV, FM_ENABLE_MODULE_WALLETV2_ENV,
16 is_env_var_set,
17};
18use fedimint_core::module::ApiAuth;
19use fedimint_core::task::{self};
20use fedimint_core::time::now;
21use fedimint_core::util::FmtCompactAnyhow as _;
22use fedimint_core::util::backoff_util::custom_backoff;
23use fedimint_logging::LOG_DEVIMINT;
24use semver::Version;
25use serde::de::DeserializeOwned;
26use tokio::fs::OpenOptions;
27use tokio::process::Child;
28use tokio::sync::Mutex;
29use tracing::debug;
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
65use crate::process_reaper;
66
67#[derive(Debug, Clone)]
72pub struct ProcessHandle(Arc<Mutex<ProcessHandleInner>>);
73
74impl ProcessHandle {
75 pub async fn terminate(&self) -> Result<()> {
76 let mut inner = self.0.lock().await;
77 inner.terminate();
78 Ok(())
79 }
80
81 pub async fn await_terminated(&self) -> Result<()> {
82 let mut inner = self.0.lock().await;
83 inner.await_terminated().await?;
84 Ok(())
85 }
86
87 pub async fn is_running(&self) -> bool {
88 self.0.lock().await.child.is_some()
89 }
90}
91
92#[derive(Debug)]
93pub struct ProcessHandleInner {
94 name: String,
95 child: Option<Child>,
96}
97
98impl ProcessHandleInner {
99 fn terminate(&mut self) {
111 if let Some(child) = self.child.take() {
112 debug!(
113 target: LOG_DEVIMINT,
114 name = %self.name,
115 "killing child process"
116 );
117
118 process_reaper::kill_process(&child);
119 process_reaper::reap_killed_processes();
120 }
121 }
122
123 async fn await_terminated(&mut self) -> anyhow::Result<()> {
124 match self
125 .child
126 .as_mut()
127 .expect("Process not running")
128 .wait()
129 .await
130 {
131 Ok(_status) => {
132 debug!(
133 target: LOG_DEVIMINT,
134 name=%self.name,
135 "child process terminated"
136 );
137 }
138 Err(err) => {
139 bail!("Failed to wait for child process {}: {}", self.name, err);
140 }
141 }
142
143 self.child.take();
145 Ok(())
146 }
147}
148
149impl Drop for ProcessHandleInner {
150 fn drop(&mut self) {
151 if self.child.is_none() {
152 return;
153 }
154
155 self.terminate();
156 }
157}
158
159#[derive(Clone)]
160pub struct ProcessManager {
161 pub globals: super::vars::Global,
162}
163
164impl ProcessManager {
165 pub fn new(globals: super::vars::Global) -> Self {
166 Self { globals }
167 }
168
169 pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
171 process_reaper::reap_killed_processes();
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 .env("RUST_LIB_BACKTRACE", "0")
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
426#[macro_export]
427macro_rules! poll_almost_equal {
428 ($left:expr_2021, $right:expr_2021) => {
429 match ($left, $right) {
430 (left, right) => $crate::util::almost_equal(left, right, 1_000_000)
431 .map_err(|e| std::ops::ControlFlow::Continue(anyhow::anyhow!(e))),
432 }
433 };
434}
435
436pub fn almost_equal(a: u64, b: u64, max: u64) -> Result<(), String> {
437 if a.abs_diff(b) <= max {
438 Ok(())
439 } else {
440 Err(format!(
441 "Expected difference is {max} but we found {}",
442 a.abs_diff(b)
443 ))
444 }
445}
446
447pub(crate) use cmd;
449
450pub async fn poll_with_timeout<Fut, R>(
457 name: &str,
458 timeout: Duration,
459 f: impl Fn() -> Fut,
460) -> Result<R>
461where
462 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
463{
464 const MIN_BACKOFF: Duration = Duration::from_millis(50);
465 const MAX_BACKOFF: Duration = Duration::from_secs(1);
466
467 let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
468 let start = now();
469 for attempt in 0u64.. {
470 let attempt_start = now();
471 match f().await {
472 Ok(value) => return Ok(value),
473 Err(ControlFlow::Break(err)) => {
474 return Err(err).with_context(|| format!("polling {name}"));
475 }
476 Err(ControlFlow::Continue(err))
477 if attempt_start
478 .duration_since(start)
479 .expect("time goes forward")
480 < timeout =>
481 {
482 debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
483 task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
484 }
485 Err(ControlFlow::Continue(err)) => {
486 return Err(err).with_context(|| {
487 format!(
488 "Polling {name} failed after {attempt} retries (timeout: {}s)",
489 timeout.as_secs()
490 )
491 });
492 }
493 }
494 }
495
496 unreachable!();
497}
498
499const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_mins(1);
500const EXTRA_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(90);
501
502pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
509where
510 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
511{
512 poll_with_timeout(
513 name,
514 if is_env_var_set("FM_EXTRA_LONG_POLL") {
515 EXTRA_LONG_POLL_TIMEOUT
516 } else {
517 DEFAULT_POLL_TIMEOUT
518 },
519 f,
520 )
521 .await
522}
523
524pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
525where
526 Fut: Future<Output = Result<R, anyhow::Error>>,
527{
528 poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
529}
530
531pub trait ToCmdExt {
533 fn cmd(self) -> Command;
534}
535
536impl ToCmdExt for &'_ str {
538 fn cmd(self) -> Command {
539 Command {
540 cmd: tokio::process::Command::new(self),
541 args_debug: vec![self.to_owned()],
542 }
543 }
544}
545
546impl ToCmdExt for Vec<String> {
547 fn cmd(self) -> Command {
548 to_command(self)
549 }
550}
551
552pub trait JsonValueExt {
553 fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
554}
555
556impl JsonValueExt for serde_json::Value {
557 fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
558 Ok(serde_json::from_value(self)?)
559 }
560}
561
562const GATEWAYD_FALLBACK: &str = "gatewayd";
563
564const FEDIMINTD_FALLBACK: &str = "fedimintd";
565
566const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
567
568pub fn get_fedimint_cli_path() -> Vec<String> {
569 get_command_str_for_alias(
570 &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
571 &[FEDIMINT_CLI_FALLBACK],
572 )
573}
574
575const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
576
577pub fn get_gateway_cli_path() -> Vec<String> {
578 get_command_str_for_alias(
579 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
580 &[GATEWAY_CLI_FALLBACK],
581 )
582}
583
584const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
585
586const LNCLI_FALLBACK: &str = "lncli";
587
588pub fn get_lncli_path() -> Vec<String> {
589 get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
590}
591
592const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
593
594pub fn get_bitcoin_cli_path() -> Vec<String> {
595 get_command_str_for_alias(
596 &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
597 &[BITCOIN_CLI_FALLBACK],
598 )
599}
600
601const BITCOIND_FALLBACK: &str = "bitcoind";
602
603const LND_FALLBACK: &str = "lnd";
604
605const ESPLORA_FALLBACK: &str = "esplora";
606
607const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
608
609const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
610
611const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
612
613pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
614 get_command_str_for_alias(
615 &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
616 &[FEDIMINT_DBTOOL_FALLBACK],
617 )
618}
619
620fn version_hash_to_version(version_hash: &str) -> Result<Version> {
622 match version_hash {
623 "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
624 "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
625 _ => Err(anyhow!("no version known for version hash: {version_hash}")),
626 }
627}
628
629pub struct FedimintdCmd;
630impl FedimintdCmd {
631 pub fn cmd(self) -> Command {
632 to_command(get_command_str_for_alias(
633 &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
634 &[FEDIMINTD_FALLBACK],
635 ))
636 }
637
638 pub async fn version_or_default() -> Version {
640 match cmd!(FedimintdCmd, "--version").out_string().await {
641 Ok(version) => parse_clap_version(&version),
642 Err(_) => cmd!(FedimintdCmd, "version-hash")
643 .out_string()
644 .await
645 .map_or(DEFAULT_VERSION, |v| {
646 version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
647 }),
648 }
649 }
650}
651
652pub struct Gatewayd;
653impl Gatewayd {
654 pub fn cmd(self) -> Command {
655 to_command(get_command_str_for_alias(
656 &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
657 &[GATEWAYD_FALLBACK],
658 ))
659 }
660
661 pub async fn version_or_default() -> Version {
663 match cmd!(Gatewayd, "--version").out_string().await {
664 Ok(version) => parse_clap_version(&version),
665 Err(_) => cmd!(Gatewayd, "version-hash")
666 .out_string()
667 .await
668 .map_or(DEFAULT_VERSION, |v| {
669 version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
670 }),
671 }
672 }
673}
674
675pub struct FedimintCli;
676impl FedimintCli {
677 pub fn cmd(self) -> Command {
678 to_command(get_command_str_for_alias(
679 &[FM_MINT_CLIENT_ENV],
680 &get_fedimint_cli_path()
681 .iter()
682 .map(String::as_str)
683 .collect::<Vec<_>>(),
684 ))
685 }
686
687 pub async fn version_or_default() -> Version {
689 match cmd!(FedimintCli, "--version").out_string().await {
690 Ok(version) => parse_clap_version(&version),
691 Err(_) => DEFAULT_VERSION,
692 }
693 }
694
695 pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
696 cmd!(
697 self,
698 "--password",
699 auth.as_str(),
700 "admin",
701 "dkg",
702 "--ws",
703 endpoint,
704 "set-password",
705 )
706 .run()
707 .await
708 }
709
710 pub async fn set_local_params_leader(
711 self,
712 peer: &PeerId,
713 auth: &ApiAuth,
714 endpoint: &str,
715 federation_size: Option<usize>,
716 ) -> Result<String> {
717 let mut command = cmd!(
718 self,
719 "--password",
720 auth.as_str(),
721 "admin",
722 "setup",
723 endpoint,
724 "set-local-params",
725 format!("Devimint Guardian {peer}"),
726 "--federation-name",
727 "Devimint Federation",
728 );
729
730 if let Some(size) = federation_size {
731 command = command.args(["--federation-size", &size.to_string()]);
732 }
733
734 let json = command.out_json().await?;
735
736 Ok(serde_json::from_value(json)?)
737 }
738
739 pub async fn set_local_params_follower(
740 self,
741 peer: &PeerId,
742 auth: &ApiAuth,
743 endpoint: &str,
744 ) -> Result<String> {
745 let json = cmd!(
746 self,
747 "--password",
748 auth.as_str(),
749 "admin",
750 "setup",
751 endpoint,
752 "set-local-params",
753 format!("Devimint Guardian {peer}")
754 )
755 .out_json()
756 .await?;
757
758 Ok(serde_json::from_value(json)?)
759 }
760
761 pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
762 cmd!(
763 self,
764 "--password",
765 auth.as_str(),
766 "admin",
767 "setup",
768 endpoint,
769 "add-peer",
770 params
771 )
772 .run()
773 .await
774 }
775
776 pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
777 let json = cmd!(
778 self,
779 "--password",
780 auth.as_str(),
781 "admin",
782 "setup",
783 endpoint,
784 "status",
785 )
786 .out_json()
787 .await?;
788
789 Ok(serde_json::from_value(json)?)
790 }
791
792 pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
793 cmd!(
794 self,
795 "--password",
796 auth.as_str(),
797 "admin",
798 "setup",
799 endpoint,
800 "start-dkg"
801 )
802 .run()
803 .await
804 }
805
806 pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
807 cmd!(
808 self,
809 "--password",
810 auth.as_str(),
811 "--our-id",
812 our_id,
813 "admin",
814 "shutdown",
815 session_count,
816 )
817 .run()
818 .await
819 }
820
821 pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
822 cmd!(
823 self,
824 "--password",
825 auth.as_str(),
826 "--our-id",
827 our_id,
828 "admin",
829 "status",
830 )
831 .run()
832 .await
833 }
834}
835
836pub struct LoadTestTool;
837impl LoadTestTool {
838 pub fn cmd(self) -> Command {
839 to_command(get_command_str_for_alias(
840 &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
841 &[LOAD_TEST_TOOL_FALLBACK],
842 ))
843 }
844}
845
846pub struct GatewayCli;
847impl GatewayCli {
848 pub fn cmd(self) -> Command {
849 to_command(get_command_str_for_alias(
850 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
851 &get_gateway_cli_path()
852 .iter()
853 .map(String::as_str)
854 .collect::<Vec<_>>(),
855 ))
856 }
857
858 pub async fn version_or_default() -> Version {
860 match cmd!(GatewayCli, "--version").out_string().await {
861 Ok(version) => parse_clap_version(&version),
862 Err(_) => DEFAULT_VERSION,
863 }
864 }
865}
866
867pub struct GatewayLndCli;
868impl GatewayLndCli {
869 pub fn cmd(self) -> Command {
870 to_command(get_command_str_for_alias(
871 &[FM_GWCLI_LND_ENV],
872 &["gateway-lnd"],
873 ))
874 }
875}
876
877pub struct GatewayLdkCli;
878impl GatewayLdkCli {
879 pub fn cmd(self) -> Command {
880 to_command(get_command_str_for_alias(
881 &[FM_GWCLI_LDK_ENV],
882 &["gateway-ldk"],
883 ))
884 }
885}
886
887pub struct LnCli;
888impl LnCli {
889 pub fn cmd(self) -> Command {
890 to_command(get_command_str_for_alias(
891 &[FM_LNCLI_ENV],
892 &get_lncli_path()
893 .iter()
894 .map(String::as_str)
895 .collect::<Vec<_>>(),
896 ))
897 }
898}
899
900pub struct BitcoinCli;
901impl BitcoinCli {
902 pub fn cmd(self) -> Command {
903 to_command(get_command_str_for_alias(
904 &[FM_BTC_CLIENT_ENV],
905 &get_bitcoin_cli_path()
906 .iter()
907 .map(String::as_str)
908 .collect::<Vec<_>>(),
909 ))
910 }
911}
912
913pub struct Bitcoind;
914impl Bitcoind {
915 pub fn cmd(self) -> Command {
916 to_command(get_command_str_for_alias(
917 &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
918 &[BITCOIND_FALLBACK],
919 ))
920 }
921}
922
923pub struct Lnd;
924impl Lnd {
925 pub fn cmd(self) -> Command {
926 to_command(get_command_str_for_alias(
927 &[FM_LND_BASE_EXECUTABLE_ENV],
928 &[LND_FALLBACK],
929 ))
930 }
931}
932
933pub struct Esplora;
934impl Esplora {
935 pub fn cmd(self) -> Command {
936 to_command(get_command_str_for_alias(
937 &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
938 &[ESPLORA_FALLBACK],
939 ))
940 }
941}
942
943pub struct Recoverytool;
944impl Recoverytool {
945 pub fn cmd(self) -> Command {
946 to_command(get_command_str_for_alias(
947 &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
948 &[RECOVERYTOOL_FALLBACK],
949 ))
950 }
951}
952
953pub struct DevimintFaucet;
954impl DevimintFaucet {
955 pub fn cmd(self) -> Command {
956 to_command(get_command_str_for_alias(
957 &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
958 &[DEVIMINT_FAUCET_FALLBACK],
959 ))
960 }
961}
962
963pub struct Recurringd;
964impl Recurringd {
965 pub fn cmd(self) -> Command {
966 to_command(get_command_str_for_alias(
967 &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
968 &["fedimint-recurringd"],
969 ))
970 }
971}
972
973fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
974 for alias in aliases {
976 if let Ok(cmd) = std::env::var(alias) {
977 return cmd.split_whitespace().map(ToOwned::to_owned).collect();
978 }
979 }
980 default.iter().map(ToString::to_string).collect()
982}
983
984fn to_command(cli: Vec<String>) -> Command {
985 let mut cmd = tokio::process::Command::new(&cli[0]);
986 cmd.args(&cli[1..]);
987 Command {
988 cmd,
989 args_debug: cli,
990 }
991}
992
993pub fn supports_lnv2() -> bool {
994 std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
995 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
996}
997
998pub fn supports_wallet_v2() -> bool {
999 is_env_var_set(FM_ENABLE_MODULE_WALLETV2_ENV)
1000}
1001
1002pub fn supports_mint_v2() -> bool {
1003 is_env_var_set(FM_ENABLE_MODULE_MINTV2_ENV)
1004}
1005
1006pub fn is_backwards_compatibility_test() -> bool {
1008 is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1009}
1010
1011pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1015 let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1016 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1017 let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1018
1019 if pkg_version == fedimintd_version {
1020 unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1025 } else {
1026 let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1027
1028 let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1030 let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1031 unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1033 }
1034
1035 let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1036 let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1037 let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1038
1039 let fm_mint_client: String = format!(
1040 "{fedimint_cli} --data-dir {datadir}",
1041 fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1042 datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1043 );
1044 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1046
1047 Ok((original_fedimint_cli_path, original_fm_mint_client))
1048}
1049
1050pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1052 unsafe {
1054 std::env::set_var(
1055 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1056 original_fedimint_cli_path,
1057 );
1058 };
1059
1060 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1062}
1063
1064fn parse_clap_version(res: &str) -> Version {
1067 match res.split(' ').collect::<Vec<&str>>().as_slice() {
1068 [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1069 _ => DEFAULT_VERSION,
1070 }
1071}
1072
1073#[test]
1074fn test_parse_clap_version() -> Result<()> {
1075 let version_str = "fedimintd 0.3.0-alpha";
1076 let expected_version = Version::parse("0.3.0-alpha")?;
1077 assert_eq!(expected_version, parse_clap_version(version_str));
1078
1079 let version_str = "fedimintd 0.3.12";
1080 let expected_version = Version::parse("0.3.12")?;
1081 assert_eq!(expected_version, parse_clap_version(version_str));
1082
1083 let version_str = "fedimint-cli 2.12.2-rc22";
1084 let expected_version = Version::parse("2.12.2-rc22")?;
1085 assert_eq!(expected_version, parse_clap_version(version_str));
1086
1087 let version_str = "bad version";
1088 let expected_version = DEFAULT_VERSION;
1089 assert_eq!(expected_version, parse_clap_version(version_str));
1090
1091 Ok(())
1092}