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