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    pub async fn id(&self) -> Option<u32> {
92        self.0.lock().await.child.as_ref().and_then(Child::id)
93    }
94}
95
96#[derive(Debug)]
97pub struct ProcessHandleInner {
98    name: String,
99    child: Option<Child>,
100}
101
102impl ProcessHandleInner {
103    /// Signal and wait for the child to be reaped.
104    ///
105    /// Blocks synchronously. This matters for:
106    /// - explicit `terminate().await` callers that `rm -rf` the data dir right
107    ///   after;
108    /// - `Drop` at program exit, so children don't outlive `devimint` and race
109    ///   post-test cleanup in `fm-run-test`.
110    ///
111    /// The wait is a plain `CondVar::wait` in the reaper — we never
112    /// `block_on` an async fn, so we don't reintroduce the panic-unsafe
113    /// executor-stall hazard that motivated this PR.
114    fn terminate(&mut self) {
115        if let Some(child) = self.child.take() {
116            debug!(
117                target: LOG_DEVIMINT,
118                name = %self.name,
119                "killing child process"
120            );
121
122            process_reaper::kill_process(&child);
123            process_reaper::reap_killed_processes();
124        }
125    }
126
127    async fn await_terminated(&mut self) -> anyhow::Result<()> {
128        match self
129            .child
130            .as_mut()
131            .expect("Process not running")
132            .wait()
133            .await
134        {
135            Ok(_status) => {
136                debug!(
137                    target: LOG_DEVIMINT,
138                    name=%self.name,
139                    "child process terminated"
140                );
141            }
142            Err(err) => {
143                bail!("Failed to wait for child process {}: {}", self.name, err);
144            }
145        }
146
147        // only drop the child handle if succeeded to terminate
148        self.child.take();
149        Ok(())
150    }
151}
152
153impl Drop for ProcessHandleInner {
154    fn drop(&mut self) {
155        if self.child.is_none() {
156            return;
157        }
158
159        self.terminate();
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    /// Logs to $FM_LOGS_DIR/{name}.{out,err}
174    pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
175        // Reap any recently killed processes so their resources
176        // (ports, file locks) are fully released before we bind new ones.
177        process_reaper::reap_killed_processes();
178        debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
179        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
180        let path = format!("{logs_dir}/{name}.log");
181        let log = OpenOptions::new()
182            .append(true)
183            .create(true)
184            .open(path)
185            .await?
186            .into_std()
187            .await;
188        cmd.cmd.kill_on_drop(false); // we handle killing ourself
189        cmd.cmd.stdout(log.try_clone()?);
190        cmd.cmd.stderr(log);
191        let child = cmd
192            .cmd
193            .spawn()
194            .with_context(|| format!("Could not spawn: {name}"))?;
195        let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
196            name: name.to_owned(),
197            child: Some(child),
198        })));
199        Ok(handle)
200    }
201}
202
203pub struct Command {
204    pub cmd: tokio::process::Command,
205    pub args_debug: Vec<String>,
206}
207
208impl Command {
209    pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
210        let string = arg.to_string();
211        self.cmd.arg(string.clone());
212        self.args_debug.push(string);
213        self
214    }
215
216    pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
217        for arg in args {
218            self = self.arg(&arg);
219        }
220        self
221    }
222
223    pub fn env<K, V>(mut self, key: K, val: V) -> Self
224    where
225        K: AsRef<OsStr>,
226        V: AsRef<OsStr>,
227    {
228        self.cmd.env(key, val);
229        self
230    }
231
232    pub fn envs<I, K, V>(mut self, env: I) -> Self
233    where
234        I: IntoIterator<Item = (K, V)>,
235        K: AsRef<OsStr>,
236        V: AsRef<OsStr>,
237    {
238        self.cmd.envs(env);
239        self
240    }
241
242    pub fn kill_on_drop(mut self, kill: bool) -> Self {
243        self.cmd.kill_on_drop(kill);
244        self
245    }
246
247    /// Run the command and get its output as json.
248    pub async fn out_json(&mut self) -> Result<serde_json::Value> {
249        Ok(serde_json::from_str(&self.out_string().await?)?)
250    }
251
252    fn command_debug(&self) -> String {
253        self.args_debug
254            .iter()
255            .map(|x| x.replace(' ', "␣"))
256            .collect::<Vec<_>>()
257            .join(" ")
258    }
259
260    /// Run the command and get its output as string.
261    pub async fn out_string(&mut self) -> Result<String> {
262        let output = self
263            .run_inner(true)
264            .await
265            .with_context(|| format!("command: {}", self.command_debug()))?;
266        let output = String::from_utf8(output.stdout)?;
267        Ok(output.trim().to_owned())
268    }
269
270    /// Returns the json error if the command has a non-zero exit code.
271    pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
272        let output = self
273            .run_inner(false)
274            .await
275            .with_context(|| format!("command: {}", self.command_debug()))?;
276        let output = String::from_utf8(output.stdout)?;
277        Ok(serde_json::from_str(output.trim())?)
278    }
279
280    /// Run the command expecting an error, which is parsed using a closure.
281    /// Returns an Err if the closure returns false.
282    pub async fn assert_error(
283        &mut self,
284        predicate: impl Fn(serde_json::Value) -> bool,
285    ) -> Result<()> {
286        let parsed_error = self.expect_err_json().await?;
287        anyhow::ensure!(predicate(parsed_error));
288        Ok(())
289    }
290
291    /// Returns an Err if the command doesn't return an error containing the
292    /// provided error string.
293    pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
294        self.assert_error(|err_json| {
295            let error_string = err_json
296                .get("error")
297                .expect("json error contains error field")
298                .as_str()
299                .expect("not a string")
300                .to_owned();
301
302            error_string.contains(error)
303        })
304        .await
305    }
306
307    pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
308        debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
309        let output = self
310            .cmd
311            .stdout(Stdio::piped())
312            .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
313                Stdio::inherit()
314            } else {
315                Stdio::piped()
316            })
317            .spawn()?
318            .wait_with_output()
319            .await?;
320
321        if output.status.success() != expect_success {
322            bail!(
323                "{}\nstdout:\n{}\nstderr:\n{}\n",
324                output.status,
325                String::from_utf8_lossy(&output.stdout),
326                String::from_utf8_lossy(&output.stderr),
327            );
328        }
329        Ok(output)
330    }
331
332    /// Run the command ignoring its output.
333    pub async fn run(&mut self) -> Result<()> {
334        let _ = self
335            .run_inner(true)
336            .await
337            .with_context(|| format!("command: {}", self.command_debug()))?;
338        Ok(())
339    }
340
341    /// Run the command logging the output and error
342    pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
343        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
344        let path = format!("{logs_dir}/{name}.log");
345        let log = OpenOptions::new()
346            .append(true)
347            .create(true)
348            .open(&path)
349            .await
350            .with_context(|| format!("path: {path} cmd: {name}"))?
351            .into_std()
352            .await;
353        self.cmd.stdout(log.try_clone()?);
354        self.cmd.stderr(log);
355        let status = self
356            .cmd
357            .spawn()
358            .with_context(|| format!("cmd: {name}"))?
359            .wait()
360            .await?;
361        if !status.success() {
362            bail!("{status}");
363        }
364        Ok(())
365    }
366}
367
368/// easy syntax to create a Command
369///
370/// `(A1, A2, A3)` expands to
371/// ```ignore
372/// A1.cmd().await?
373///     .arg(A2)
374///     .arg(A3)
375///     .kill_on_drop(true)
376/// ```
377///
378/// If `An` is a string literal, it is replaced with `format!(a)`
379#[macro_export]
380macro_rules! cmd {
381    ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
382        cmd! {
383            @head ($($($head)*)? format!($curr),)
384            $($($tail)*)?
385        }
386    };
387    ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
388        cmd! {
389            @head ($($($head)*)? $curr,)
390            $($($tail)*)?
391        }
392    };
393    (@head ($($head:tt)* )) => {
394        cmd! {
395            @last
396            $($head)*
397        }
398    };
399    // last matcher
400    (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
401        {
402            #[allow(unused)]
403            use $crate::util::ToCmdExt;
404            $this.cmd()
405                $(.arg(&$arg))*
406                .kill_on_drop(true)
407                .env("RUST_BACKTRACE", "1")
408                .env("RUST_LIB_BACKTRACE", "0")
409        }
410    };
411}
412
413#[macro_export]
414macro_rules! poll_eq {
415    ($left:expr_2021, $right:expr_2021) => {
416        match ($left, $right) {
417            (left, right) => {
418                if left == right {
419                    Ok(())
420                } else {
421                    Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
422                        "assertion failed, left: {left:?} right: {right:?}"
423                    )))
424                }
425            }
426        }
427    };
428}
429
430#[macro_export]
431macro_rules! poll_almost_equal {
432    ($left:expr_2021, $right:expr_2021) => {
433        match ($left, $right) {
434            (left, right) => $crate::util::almost_equal(left, right, 1_000_000)
435                .map_err(|e| std::ops::ControlFlow::Continue(anyhow::anyhow!(e))),
436        }
437    };
438}
439
440pub fn almost_equal(a: u64, b: u64, max: u64) -> Result<(), String> {
441    if a.abs_diff(b) <= max {
442        Ok(())
443    } else {
444        Err(format!(
445            "Expected difference is {max} but we found {}",
446            a.abs_diff(b)
447        ))
448    }
449}
450
451// Allow macro to be used within the crate. See https://stackoverflow.com/a/31749071.
452pub(crate) use cmd;
453
454/// Retry until `f` succeeds or timeout is reached
455///
456/// - if `f` return Ok(val), this returns with Ok(val).
457/// - if `f` return Err(Control::Break(err)), this returns Err(err)
458/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
459///   reached
460pub async fn poll_with_timeout<Fut, R>(
461    name: &str,
462    timeout: Duration,
463    f: impl Fn() -> Fut,
464) -> Result<R>
465where
466    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
467{
468    const MIN_BACKOFF: Duration = Duration::from_millis(50);
469    const MAX_BACKOFF: Duration = Duration::from_secs(1);
470
471    let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
472    let start = now();
473    for attempt in 0u64.. {
474        let attempt_start = now();
475        match f().await {
476            Ok(value) => return Ok(value),
477            Err(ControlFlow::Break(err)) => {
478                return Err(err).with_context(|| format!("polling {name}"));
479            }
480            Err(ControlFlow::Continue(err))
481                if attempt_start
482                    .duration_since(start)
483                    .expect("time goes forward")
484                    < timeout =>
485            {
486                debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
487                task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
488            }
489            Err(ControlFlow::Continue(err)) => {
490                return Err(err).with_context(|| {
491                    format!(
492                        "Polling {name} failed after {attempt} retries (timeout: {}s)",
493                        timeout.as_secs()
494                    )
495                });
496            }
497        }
498    }
499
500    unreachable!();
501}
502
503const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_mins(1);
504const EXTRA_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(90);
505
506/// Retry until `f` succeeds or default timeout is reached
507///
508/// - if `f` return Ok(val), this returns with Ok(val).
509/// - if `f` return Err(Control::Break(err)), this returns Err(err)
510/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
511///   reached
512pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
513where
514    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
515{
516    poll_with_timeout(
517        name,
518        if is_env_var_set("FM_EXTRA_LONG_POLL") {
519            EXTRA_LONG_POLL_TIMEOUT
520        } else {
521            DEFAULT_POLL_TIMEOUT
522        },
523        f,
524    )
525    .await
526}
527
528pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
529where
530    Fut: Future<Output = Result<R, anyhow::Error>>,
531{
532    poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
533}
534
535// used to add `cmd` method.
536pub trait ToCmdExt {
537    fn cmd(self) -> Command;
538}
539
540// a command that uses self as program name
541impl ToCmdExt for &'_ str {
542    fn cmd(self) -> Command {
543        Command {
544            cmd: tokio::process::Command::new(self),
545            args_debug: vec![self.to_owned()],
546        }
547    }
548}
549
550impl ToCmdExt for Vec<String> {
551    fn cmd(self) -> Command {
552        to_command(self)
553    }
554}
555
556pub trait JsonValueExt {
557    fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
558}
559
560impl JsonValueExt for serde_json::Value {
561    fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
562        Ok(serde_json::from_value(self)?)
563    }
564}
565
566const GATEWAYD_FALLBACK: &str = "gatewayd";
567
568const FEDIMINTD_FALLBACK: &str = "fedimintd";
569
570const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
571
572pub fn get_fedimint_cli_path() -> Vec<String> {
573    get_command_str_for_alias(
574        &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
575        &[FEDIMINT_CLI_FALLBACK],
576    )
577}
578
579const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
580
581pub fn get_gateway_cli_path() -> Vec<String> {
582    get_command_str_for_alias(
583        &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
584        &[GATEWAY_CLI_FALLBACK],
585    )
586}
587
588const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
589
590const LNCLI_FALLBACK: &str = "lncli";
591
592pub fn get_lncli_path() -> Vec<String> {
593    get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
594}
595
596const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
597
598pub fn get_bitcoin_cli_path() -> Vec<String> {
599    get_command_str_for_alias(
600        &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
601        &[BITCOIN_CLI_FALLBACK],
602    )
603}
604
605const BITCOIND_FALLBACK: &str = "bitcoind";
606
607const LND_FALLBACK: &str = "lnd";
608
609const ESPLORA_FALLBACK: &str = "esplora";
610
611const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
612
613const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
614
615const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
616
617pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
618    get_command_str_for_alias(
619        &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
620        &[FEDIMINT_DBTOOL_FALLBACK],
621    )
622}
623
624/// Maps a version hash to a release version
625fn version_hash_to_version(version_hash: &str) -> Result<Version> {
626    match version_hash {
627        "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
628        "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
629        _ => Err(anyhow!("no version known for version hash: {version_hash}")),
630    }
631}
632
633pub struct FedimintdCmd;
634impl FedimintdCmd {
635    pub fn cmd(self) -> Command {
636        to_command(get_command_str_for_alias(
637            &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
638            &[FEDIMINTD_FALLBACK],
639        ))
640    }
641
642    /// Returns the fedimintd version from clap or default min version
643    pub async fn version_or_default() -> Version {
644        match cmd!(FedimintdCmd, "--version").out_string().await {
645            Ok(version) => parse_clap_version(&version),
646            Err(_) => cmd!(FedimintdCmd, "version-hash")
647                .out_string()
648                .await
649                .map_or(DEFAULT_VERSION, |v| {
650                    version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
651                }),
652        }
653    }
654}
655
656pub struct Gatewayd;
657impl Gatewayd {
658    pub fn cmd(self) -> Command {
659        to_command(get_command_str_for_alias(
660            &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
661            &[GATEWAYD_FALLBACK],
662        ))
663    }
664
665    /// Returns the gatewayd version from clap or default min version
666    pub async fn version_or_default() -> Version {
667        match cmd!(Gatewayd, "--version").out_string().await {
668            Ok(version) => parse_clap_version(&version),
669            Err(_) => cmd!(Gatewayd, "version-hash")
670                .out_string()
671                .await
672                .map_or(DEFAULT_VERSION, |v| {
673                    version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
674                }),
675        }
676    }
677}
678
679pub struct FedimintCli;
680impl FedimintCli {
681    pub fn cmd(self) -> Command {
682        to_command(get_command_str_for_alias(
683            &[FM_MINT_CLIENT_ENV],
684            &get_fedimint_cli_path()
685                .iter()
686                .map(String::as_str)
687                .collect::<Vec<_>>(),
688        ))
689    }
690
691    /// Returns the fedimint-cli version from clap or default min version
692    pub async fn version_or_default() -> Version {
693        match cmd!(FedimintCli, "--version").out_string().await {
694            Ok(version) => parse_clap_version(&version),
695            Err(_) => DEFAULT_VERSION,
696        }
697    }
698
699    pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
700        cmd!(
701            self,
702            "--password",
703            auth.as_str(),
704            "admin",
705            "dkg",
706            "--ws",
707            endpoint,
708            "set-password",
709        )
710        .run()
711        .await
712    }
713
714    pub async fn set_local_params_leader(
715        self,
716        peer: &PeerId,
717        auth: &ApiAuth,
718        endpoint: &str,
719        federation_size: Option<usize>,
720    ) -> Result<String> {
721        let mut command = cmd!(
722            self,
723            "--password",
724            auth.as_str(),
725            "admin",
726            "setup",
727            endpoint,
728            "set-local-params",
729            format!("Devimint Guardian {peer}"),
730            "--federation-name",
731            "Devimint Federation",
732        );
733
734        if let Some(size) = federation_size {
735            command = command.args(["--federation-size", &size.to_string()]);
736        }
737
738        let json = command.out_json().await?;
739
740        Ok(serde_json::from_value(json)?)
741    }
742
743    pub async fn set_local_params_follower(
744        self,
745        peer: &PeerId,
746        auth: &ApiAuth,
747        endpoint: &str,
748    ) -> Result<String> {
749        let json = cmd!(
750            self,
751            "--password",
752            auth.as_str(),
753            "admin",
754            "setup",
755            endpoint,
756            "set-local-params",
757            format!("Devimint Guardian {peer}")
758        )
759        .out_json()
760        .await?;
761
762        Ok(serde_json::from_value(json)?)
763    }
764
765    pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
766        cmd!(
767            self,
768            "--password",
769            auth.as_str(),
770            "admin",
771            "setup",
772            endpoint,
773            "add-peer",
774            params
775        )
776        .run()
777        .await
778    }
779
780    pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
781        let json = cmd!(
782            self,
783            "--password",
784            auth.as_str(),
785            "admin",
786            "setup",
787            endpoint,
788            "status",
789        )
790        .out_json()
791        .await?;
792
793        Ok(serde_json::from_value(json)?)
794    }
795
796    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
797        cmd!(
798            self,
799            "--password",
800            auth.as_str(),
801            "admin",
802            "setup",
803            endpoint,
804            "start-dkg"
805        )
806        .run()
807        .await
808    }
809
810    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
811        cmd!(
812            self,
813            "--password",
814            auth.as_str(),
815            "--our-id",
816            our_id,
817            "admin",
818            "shutdown",
819            session_count,
820        )
821        .run()
822        .await
823    }
824
825    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
826        cmd!(
827            self,
828            "--password",
829            auth.as_str(),
830            "--our-id",
831            our_id,
832            "admin",
833            "status",
834        )
835        .run()
836        .await
837    }
838}
839
840pub struct LoadTestTool;
841impl LoadTestTool {
842    pub fn cmd(self) -> Command {
843        to_command(get_command_str_for_alias(
844            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
845            &[LOAD_TEST_TOOL_FALLBACK],
846        ))
847    }
848}
849
850pub struct GatewayCli;
851impl GatewayCli {
852    pub fn cmd(self) -> Command {
853        to_command(get_command_str_for_alias(
854            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
855            &get_gateway_cli_path()
856                .iter()
857                .map(String::as_str)
858                .collect::<Vec<_>>(),
859        ))
860    }
861
862    /// Returns the gateway-cli version from clap or default min version
863    pub async fn version_or_default() -> Version {
864        match cmd!(GatewayCli, "--version").out_string().await {
865            Ok(version) => parse_clap_version(&version),
866            Err(_) => DEFAULT_VERSION,
867        }
868    }
869}
870
871pub struct GatewayLndCli;
872impl GatewayLndCli {
873    pub fn cmd(self) -> Command {
874        to_command(get_command_str_for_alias(
875            &[FM_GWCLI_LND_ENV],
876            &["gateway-lnd"],
877        ))
878    }
879}
880
881pub struct GatewayLdkCli;
882impl GatewayLdkCli {
883    pub fn cmd(self) -> Command {
884        to_command(get_command_str_for_alias(
885            &[FM_GWCLI_LDK_ENV],
886            &["gateway-ldk"],
887        ))
888    }
889}
890
891pub struct LnCli;
892impl LnCli {
893    pub fn cmd(self) -> Command {
894        to_command(get_command_str_for_alias(
895            &[FM_LNCLI_ENV],
896            &get_lncli_path()
897                .iter()
898                .map(String::as_str)
899                .collect::<Vec<_>>(),
900        ))
901    }
902}
903
904pub struct BitcoinCli;
905impl BitcoinCli {
906    pub fn cmd(self) -> Command {
907        to_command(get_command_str_for_alias(
908            &[FM_BTC_CLIENT_ENV],
909            &get_bitcoin_cli_path()
910                .iter()
911                .map(String::as_str)
912                .collect::<Vec<_>>(),
913        ))
914    }
915}
916
917pub struct Bitcoind;
918impl Bitcoind {
919    pub fn cmd(self) -> Command {
920        to_command(get_command_str_for_alias(
921            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
922            &[BITCOIND_FALLBACK],
923        ))
924    }
925}
926
927pub struct Lnd;
928impl Lnd {
929    pub fn cmd(self) -> Command {
930        to_command(get_command_str_for_alias(
931            &[FM_LND_BASE_EXECUTABLE_ENV],
932            &[LND_FALLBACK],
933        ))
934    }
935}
936
937pub struct Esplora;
938impl Esplora {
939    pub fn cmd(self) -> Command {
940        to_command(get_command_str_for_alias(
941            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
942            &[ESPLORA_FALLBACK],
943        ))
944    }
945}
946
947pub struct Recoverytool;
948impl Recoverytool {
949    pub fn cmd(self) -> Command {
950        to_command(get_command_str_for_alias(
951            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
952            &[RECOVERYTOOL_FALLBACK],
953        ))
954    }
955}
956
957pub struct DevimintFaucet;
958impl DevimintFaucet {
959    pub fn cmd(self) -> Command {
960        to_command(get_command_str_for_alias(
961            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
962            &[DEVIMINT_FAUCET_FALLBACK],
963        ))
964    }
965}
966
967pub struct Recurringd;
968impl Recurringd {
969    pub fn cmd(self) -> Command {
970        to_command(get_command_str_for_alias(
971            &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
972            &["fedimint-recurringd"],
973        ))
974    }
975}
976
977fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
978    // try to use one of the aliases if set
979    for alias in aliases {
980        if let Ok(cmd) = std::env::var(alias) {
981            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
982        }
983    }
984    // otherwise return the default value
985    default.iter().map(ToString::to_string).collect()
986}
987
988fn to_command(cli: Vec<String>) -> Command {
989    let mut cmd = tokio::process::Command::new(&cli[0]);
990    cmd.args(&cli[1..]);
991    Command {
992        cmd,
993        args_debug: cli,
994    }
995}
996
997pub fn supports_lnv2() -> bool {
998    std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
999        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1000}
1001
1002pub fn supports_wallet_v2() -> bool {
1003    is_env_var_set(FM_ENABLE_MODULE_WALLETV2_ENV)
1004}
1005
1006pub fn supports_mint_v2() -> bool {
1007    is_env_var_set(FM_ENABLE_MODULE_MINTV2_ENV)
1008}
1009
1010/// Returns true if running backwards-compatibility tests
1011pub fn is_backwards_compatibility_test() -> bool {
1012    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1013}
1014
1015/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1016/// needed for running DKG. Returns the original fedimint-cli path and mint
1017/// client alias so the caller can reset the fedimint-cli version after DKG
1018pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1019    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1020    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1021    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1022
1023    if pkg_version == fedimintd_version {
1024        // we're on the current version if the fedimintd version is the same as the
1025        // package version. to use the current version of `fedimint-cli` built by cargo,
1026        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1027        // TODO: Audit that the environment access only happens in single-threaded code.
1028        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1029    } else {
1030        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1031
1032        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1033        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1034        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1035        // TODO: Audit that the environment access only happens in single-threaded code.
1036        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1037    }
1038
1039    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1040    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1041    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1042
1043    let fm_mint_client: String = format!(
1044        "{fedimint_cli} --data-dir {datadir}",
1045        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1046        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1047    );
1048    // TODO: Audit that the environment access only happens in single-threaded code.
1049    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1050
1051    Ok((original_fedimint_cli_path, original_fm_mint_client))
1052}
1053
1054/// Sets the fedimint-cli and mint client alias
1055pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1056    // TODO: Audit that the environment access only happens in single-threaded code.
1057    unsafe {
1058        std::env::set_var(
1059            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1060            original_fedimint_cli_path,
1061        );
1062    };
1063
1064    // TODO: Audit that the environment access only happens in single-threaded code.
1065    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1066}
1067
1068/// Parses a version string returned from clap
1069/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1070fn parse_clap_version(res: &str) -> Version {
1071    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1072        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1073        _ => DEFAULT_VERSION,
1074    }
1075}
1076
1077#[test]
1078fn test_parse_clap_version() -> Result<()> {
1079    let version_str = "fedimintd 0.3.0-alpha";
1080    let expected_version = Version::parse("0.3.0-alpha")?;
1081    assert_eq!(expected_version, parse_clap_version(version_str));
1082
1083    let version_str = "fedimintd 0.3.12";
1084    let expected_version = Version::parse("0.3.12")?;
1085    assert_eq!(expected_version, parse_clap_version(version_str));
1086
1087    let version_str = "fedimint-cli 2.12.2-rc22";
1088    let expected_version = Version::parse("2.12.2-rc22")?;
1089    assert_eq!(expected_version, parse_clap_version(version_str));
1090
1091    let version_str = "bad version";
1092    let expected_version = DEFAULT_VERSION;
1093    assert_eq!(expected_version, parse_clap_version(version_str));
1094
1095    Ok(())
1096}