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_local_params_leader(
700        self,
701        peer: &PeerId,
702        auth: &ApiAuth,
703        endpoint: &str,
704        federation_size: Option<usize>,
705    ) -> Result<String> {
706        let mut command = cmd!(
707            self,
708            "--password",
709            auth.as_str(),
710            "admin",
711            "setup",
712            endpoint,
713            "set-local-params",
714            format!("Devimint Guardian {peer}"),
715            "--federation-name",
716            "Devimint Federation",
717        );
718
719        if let Some(size) = federation_size {
720            command = command.args(["--federation-size", &size.to_string()]);
721        }
722
723        let json = command.out_json().await?;
724
725        Ok(serde_json::from_value(json)?)
726    }
727
728    pub async fn set_local_params_follower(
729        self,
730        peer: &PeerId,
731        auth: &ApiAuth,
732        endpoint: &str,
733    ) -> Result<String> {
734        let json = cmd!(
735            self,
736            "--password",
737            auth.as_str(),
738            "admin",
739            "setup",
740            endpoint,
741            "set-local-params",
742            format!("Devimint Guardian {peer}")
743        )
744        .out_json()
745        .await?;
746
747        Ok(serde_json::from_value(json)?)
748    }
749
750    pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
751        cmd!(
752            self,
753            "--password",
754            auth.as_str(),
755            "admin",
756            "setup",
757            endpoint,
758            "add-peer",
759            params
760        )
761        .run()
762        .await
763    }
764
765    pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
766        let json = cmd!(
767            self,
768            "--password",
769            auth.as_str(),
770            "admin",
771            "setup",
772            endpoint,
773            "status",
774        )
775        .out_json()
776        .await?;
777
778        Ok(serde_json::from_value(json)?)
779    }
780
781    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
782        cmd!(
783            self,
784            "--password",
785            auth.as_str(),
786            "admin",
787            "setup",
788            endpoint,
789            "start-dkg"
790        )
791        .run()
792        .await
793    }
794
795    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
796        cmd!(
797            self,
798            "--password",
799            auth.as_str(),
800            "--our-id",
801            our_id,
802            "admin",
803            "shutdown",
804            session_count,
805        )
806        .run()
807        .await
808    }
809
810    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
811        cmd!(
812            self,
813            "--password",
814            auth.as_str(),
815            "--our-id",
816            our_id,
817            "admin",
818            "status",
819        )
820        .run()
821        .await
822    }
823}
824
825pub struct LoadTestTool;
826impl LoadTestTool {
827    pub fn cmd(self) -> Command {
828        to_command(get_command_str_for_alias(
829            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
830            &[LOAD_TEST_TOOL_FALLBACK],
831        ))
832    }
833}
834
835pub struct GatewayCli;
836impl GatewayCli {
837    pub fn cmd(self) -> Command {
838        to_command(get_command_str_for_alias(
839            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
840            &get_gateway_cli_path()
841                .iter()
842                .map(String::as_str)
843                .collect::<Vec<_>>(),
844        ))
845    }
846
847    /// Returns the gateway-cli version from clap or default min version
848    pub async fn version_or_default() -> Version {
849        match cmd!(GatewayCli, "--version").out_string().await {
850            Ok(version) => parse_clap_version(&version),
851            Err(_) => DEFAULT_VERSION,
852        }
853    }
854}
855
856pub struct GatewayLndCli;
857impl GatewayLndCli {
858    pub fn cmd(self) -> Command {
859        to_command(get_command_str_for_alias(
860            &[FM_GWCLI_LND_ENV],
861            &["gateway-lnd"],
862        ))
863    }
864}
865
866pub struct GatewayLdkCli;
867impl GatewayLdkCli {
868    pub fn cmd(self) -> Command {
869        to_command(get_command_str_for_alias(
870            &[FM_GWCLI_LDK_ENV],
871            &["gateway-ldk"],
872        ))
873    }
874}
875
876pub struct LnCli;
877impl LnCli {
878    pub fn cmd(self) -> Command {
879        to_command(get_command_str_for_alias(
880            &[FM_LNCLI_ENV],
881            &get_lncli_path()
882                .iter()
883                .map(String::as_str)
884                .collect::<Vec<_>>(),
885        ))
886    }
887}
888
889pub struct BitcoinCli;
890impl BitcoinCli {
891    pub fn cmd(self) -> Command {
892        to_command(get_command_str_for_alias(
893            &[FM_BTC_CLIENT_ENV],
894            &get_bitcoin_cli_path()
895                .iter()
896                .map(String::as_str)
897                .collect::<Vec<_>>(),
898        ))
899    }
900}
901
902pub struct Bitcoind;
903impl Bitcoind {
904    pub fn cmd(self) -> Command {
905        to_command(get_command_str_for_alias(
906            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
907            &[BITCOIND_FALLBACK],
908        ))
909    }
910}
911
912pub struct Lnd;
913impl Lnd {
914    pub fn cmd(self) -> Command {
915        to_command(get_command_str_for_alias(
916            &[FM_LND_BASE_EXECUTABLE_ENV],
917            &[LND_FALLBACK],
918        ))
919    }
920}
921
922pub struct Esplora;
923impl Esplora {
924    pub fn cmd(self) -> Command {
925        to_command(get_command_str_for_alias(
926            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
927            &[ESPLORA_FALLBACK],
928        ))
929    }
930}
931
932pub struct Recoverytool;
933impl Recoverytool {
934    pub fn cmd(self) -> Command {
935        to_command(get_command_str_for_alias(
936            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
937            &[RECOVERYTOOL_FALLBACK],
938        ))
939    }
940}
941
942pub struct DevimintFaucet;
943impl DevimintFaucet {
944    pub fn cmd(self) -> Command {
945        to_command(get_command_str_for_alias(
946            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
947            &[DEVIMINT_FAUCET_FALLBACK],
948        ))
949    }
950}
951
952pub struct Recurringd;
953impl Recurringd {
954    pub fn cmd(self) -> Command {
955        to_command(get_command_str_for_alias(
956            &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
957            &["fedimint-recurringd"],
958        ))
959    }
960}
961
962fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
963    // try to use one of the aliases if set
964    for alias in aliases {
965        if let Ok(cmd) = std::env::var(alias) {
966            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
967        }
968    }
969    // otherwise return the default value
970    default.iter().map(ToString::to_string).collect()
971}
972
973fn to_command(cli: Vec<String>) -> Command {
974    let mut cmd = tokio::process::Command::new(&cli[0]);
975    cmd.args(&cli[1..]);
976    Command {
977        cmd,
978        args_debug: cli,
979    }
980}
981
982pub fn supports_lnv2() -> bool {
983    std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
984        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
985}
986
987pub fn supports_wallet_v2() -> bool {
988    is_env_var_set(FM_ENABLE_MODULE_WALLETV2_ENV)
989}
990
991pub fn supports_mint_v2() -> bool {
992    is_env_var_set(FM_ENABLE_MODULE_MINTV2_ENV)
993}
994
995/// Returns true if running backwards-compatibility tests
996pub fn is_backwards_compatibility_test() -> bool {
997    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
998}
999
1000/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1001/// needed for running DKG. Returns the original fedimint-cli path and mint
1002/// client alias so the caller can reset the fedimint-cli version after DKG
1003pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1004    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1005    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1006    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1007
1008    if pkg_version == fedimintd_version {
1009        // we're on the current version if the fedimintd version is the same as the
1010        // package version. to use the current version of `fedimint-cli` built by cargo,
1011        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1012        // TODO: Audit that the environment access only happens in single-threaded code.
1013        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1014    } else {
1015        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1016
1017        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1018        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1019        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1020        // TODO: Audit that the environment access only happens in single-threaded code.
1021        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1022    }
1023
1024    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1025    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1026    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1027
1028    let fm_mint_client: String = format!(
1029        "{fedimint_cli} --data-dir {datadir}",
1030        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1031        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1032    );
1033    // TODO: Audit that the environment access only happens in single-threaded code.
1034    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1035
1036    Ok((original_fedimint_cli_path, original_fm_mint_client))
1037}
1038
1039/// Sets the fedimint-cli and mint client alias
1040pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1041    // TODO: Audit that the environment access only happens in single-threaded code.
1042    unsafe {
1043        std::env::set_var(
1044            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1045            original_fedimint_cli_path,
1046        );
1047    };
1048
1049    // TODO: Audit that the environment access only happens in single-threaded code.
1050    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1051}
1052
1053/// Parses a version string returned from clap
1054/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1055fn parse_clap_version(res: &str) -> Version {
1056    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1057        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1058        _ => DEFAULT_VERSION,
1059    }
1060}
1061
1062#[test]
1063fn test_parse_clap_version() -> Result<()> {
1064    let version_str = "fedimintd 0.3.0-alpha";
1065    let expected_version = Version::parse("0.3.0-alpha")?;
1066    assert_eq!(expected_version, parse_clap_version(version_str));
1067
1068    let version_str = "fedimintd 0.3.12";
1069    let expected_version = Version::parse("0.3.12")?;
1070    assert_eq!(expected_version, parse_clap_version(version_str));
1071
1072    let version_str = "fedimint-cli 2.12.2-rc22";
1073    let expected_version = Version::parse("2.12.2-rc22")?;
1074    assert_eq!(expected_version, parse_clap_version(version_str));
1075
1076    let version_str = "bad version";
1077    let expected_version = DEFAULT_VERSION;
1078    assert_eq!(expected_version, parse_clap_version(version_str));
1079
1080    Ok(())
1081}