Skip to main content

devimint/
util.rs

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
43// If a binary doesn't provide a clap version, default to the first stable
44// release (v0.2.1)
45const 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/// Kills process when all references to ProcessHandle are dropped.
68///
69/// NOTE: drop order is significant make sure fields in struct are declared in
70/// correct order it is generally clients, process handle, deps
71#[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    /// Signal and wait for the child to be reaped.
100    ///
101    /// Blocks synchronously. This matters for:
102    /// - explicit `terminate().await` callers that `rm -rf` the data dir right
103    ///   after;
104    /// - `Drop` at program exit, so children don't outlive `devimint` and race
105    ///   post-test cleanup in `fm-run-test`.
106    ///
107    /// The wait is a plain `CondVar::wait` in the reaper — we never
108    /// `block_on` an async fn, so we don't reintroduce the panic-unsafe
109    /// executor-stall hazard that motivated this PR.
110    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        // only drop the child handle if succeeded to terminate
144        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    /// Logs to $FM_LOGS_DIR/{name}.{out,err}
170    pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
171        // Reap any recently killed processes so their resources
172        // (ports, file locks) are fully released before we bind new ones.
173        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); // we handle killing ourself
185        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    /// Run the command and get its output as json.
244    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    /// Run the command and get its output as string.
257    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    /// Returns the json error if the command has a non-zero exit code.
267    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    /// Run the command expecting an error, which is parsed using a closure.
277    /// Returns an Err if the closure returns false.
278    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    /// Returns an Err if the command doesn't return an error containing the
288    /// provided error string.
289    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    /// Run the command ignoring its output.
329    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    /// Run the command logging the output and error
338    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/// easy syntax to create a Command
365///
366/// `(A1, A2, A3)` expands to
367/// ```ignore
368/// A1.cmd().await?
369///     .arg(A2)
370///     .arg(A3)
371///     .kill_on_drop(true)
372/// ```
373///
374/// If `An` is a string literal, it is replaced with `format!(a)`
375#[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 matcher
396    (@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
447// Allow macro to be used within the crate. See https://stackoverflow.com/a/31749071.
448pub(crate) use cmd;
449
450/// Retry until `f` succeeds or timeout is reached
451///
452/// - if `f` return Ok(val), this returns with Ok(val).
453/// - if `f` return Err(Control::Break(err)), this returns Err(err)
454/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
455///   reached
456pub 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
502/// Retry until `f` succeeds or default timeout is reached
503///
504/// - if `f` return Ok(val), this returns with Ok(val).
505/// - if `f` return Err(Control::Break(err)), this returns Err(err)
506/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
507///   reached
508pub 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
531// used to add `cmd` method.
532pub trait ToCmdExt {
533    fn cmd(self) -> Command;
534}
535
536// a command that uses self as program name
537impl 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
620/// Maps a version hash to a release version
621fn 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    /// Returns the fedimintd version from clap or default min version
639    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    /// Returns the gatewayd version from clap or default min version
662    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    /// Returns the fedimint-cli version from clap or default min version
688    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    /// Returns the gateway-cli version from clap or default min version
859    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    // try to use one of the aliases if set
975    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    // otherwise return the default value
981    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
1006/// Returns true if running backwards-compatibility tests
1007pub fn is_backwards_compatibility_test() -> bool {
1008    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1009}
1010
1011/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1012/// needed for running DKG. Returns the original fedimint-cli path and mint
1013/// client alias so the caller can reset the fedimint-cli version after DKG
1014pub 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        // we're on the current version if the fedimintd version is the same as the
1021        // package version. to use the current version of `fedimint-cli` built by cargo,
1022        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1023        // TODO: Audit that the environment access only happens in single-threaded code.
1024        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        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1029        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        // TODO: Audit that the environment access only happens in single-threaded code.
1032        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    // TODO: Audit that the environment access only happens in single-threaded code.
1045    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
1050/// Sets the fedimint-cli and mint client alias
1051pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1052    // TODO: Audit that the environment access only happens in single-threaded code.
1053    unsafe {
1054        std::env::set_var(
1055            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1056            original_fedimint_cli_path,
1057        );
1058    };
1059
1060    // TODO: Audit that the environment access only happens in single-threaded code.
1061    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1062}
1063
1064/// Parses a version string returned from clap
1065/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1066fn 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}