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_api_client::api::StatusResponse;
13use fedimint_core::PeerId;
14use fedimint_core::admin_client::{PeerServerParamsLegacy, SetupStatus};
15use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
16use fedimint_core::envs::{FM_ENABLE_MODULE_LNV2_ENV, is_env_var_set};
17use fedimint_core::module::ApiAuth;
18use fedimint_core::task::{self, block_in_place, block_on};
19use fedimint_core::time::now;
20use fedimint_core::util::FmtCompactAnyhow as _;
21use fedimint_core::util::backoff_util::custom_backoff;
22use fedimint_logging::LOG_DEVIMINT;
23use legacy_types::ConfigGenParamsResponseLegacy;
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        block_in_place(|| {
184            if let Err(err) = block_on(self.terminate()) {
185                warn!(target: LOG_DEVIMINT,
186                        name=%self.name,
187                        err = %err.fmt_compact_anyhow(),
188                        "Error terminating process on drop");
189            }
190        });
191    }
192}
193
194#[derive(Clone)]
195pub struct ProcessManager {
196    pub globals: super::vars::Global,
197}
198
199impl ProcessManager {
200    pub fn new(globals: super::vars::Global) -> Self {
201        Self { globals }
202    }
203
204    /// Logs to $FM_LOGS_DIR/{name}.{out,err}
205    pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
206        debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
207        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
208        let path = format!("{logs_dir}/{name}.log");
209        let log = OpenOptions::new()
210            .append(true)
211            .create(true)
212            .open(path)
213            .await?
214            .into_std()
215            .await;
216        cmd.cmd.kill_on_drop(false); // we handle killing ourself
217        cmd.cmd.stdout(log.try_clone()?);
218        cmd.cmd.stderr(log);
219        let child = cmd
220            .cmd
221            .spawn()
222            .with_context(|| format!("Could not spawn: {name}"))?;
223        let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
224            name: name.to_owned(),
225            child: Some(child),
226        })));
227        Ok(handle)
228    }
229}
230
231pub struct Command {
232    pub cmd: tokio::process::Command,
233    pub args_debug: Vec<String>,
234}
235
236impl Command {
237    pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
238        let string = arg.to_string();
239        self.cmd.arg(string.clone());
240        self.args_debug.push(string);
241        self
242    }
243
244    pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
245        for arg in args {
246            self = self.arg(&arg);
247        }
248        self
249    }
250
251    pub fn env<K, V>(mut self, key: K, val: V) -> Self
252    where
253        K: AsRef<OsStr>,
254        V: AsRef<OsStr>,
255    {
256        self.cmd.env(key, val);
257        self
258    }
259
260    pub fn envs<I, K, V>(mut self, env: I) -> Self
261    where
262        I: IntoIterator<Item = (K, V)>,
263        K: AsRef<OsStr>,
264        V: AsRef<OsStr>,
265    {
266        self.cmd.envs(env);
267        self
268    }
269
270    pub fn kill_on_drop(mut self, kill: bool) -> Self {
271        self.cmd.kill_on_drop(kill);
272        self
273    }
274
275    /// Run the command and get its output as json.
276    pub async fn out_json(&mut self) -> Result<serde_json::Value> {
277        Ok(serde_json::from_str(&self.out_string().await?)?)
278    }
279
280    fn command_debug(&self) -> String {
281        self.args_debug
282            .iter()
283            .map(|x| x.replace(' ', "␣"))
284            .collect::<Vec<_>>()
285            .join(" ")
286    }
287
288    /// Run the command and get its output as string.
289    pub async fn out_string(&mut self) -> Result<String> {
290        let output = self
291            .run_inner(true)
292            .await
293            .with_context(|| format!("command: {}", self.command_debug()))?;
294        let output = String::from_utf8(output.stdout)?;
295        Ok(output.trim().to_owned())
296    }
297
298    /// Returns the json error if the command has a non-zero exit code.
299    pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
300        let output = self
301            .run_inner(false)
302            .await
303            .with_context(|| format!("command: {}", self.command_debug()))?;
304        let output = String::from_utf8(output.stdout)?;
305        Ok(serde_json::from_str(output.trim())?)
306    }
307
308    /// Run the command expecting an error, which is parsed using a closure.
309    /// Returns an Err if the closure returns false.
310    pub async fn assert_error(
311        &mut self,
312        predicate: impl Fn(serde_json::Value) -> bool,
313    ) -> Result<()> {
314        let parsed_error = self.expect_err_json().await?;
315        anyhow::ensure!(predicate(parsed_error));
316        Ok(())
317    }
318
319    /// Returns an Err if the command doesn't return an error containing the
320    /// provided error string.
321    pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
322        self.assert_error(|err_json| {
323            let error_string = err_json
324                .get("error")
325                .expect("json error contains error field")
326                .as_str()
327                .expect("not a string")
328                .to_owned();
329
330            error_string.contains(error)
331        })
332        .await
333    }
334
335    pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
336        debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
337        let output = self
338            .cmd
339            .stdout(Stdio::piped())
340            .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
341                Stdio::inherit()
342            } else {
343                Stdio::piped()
344            })
345            .spawn()?
346            .wait_with_output()
347            .await?;
348
349        if output.status.success() != expect_success {
350            bail!(
351                "{}\nstdout:\n{}\nstderr:\n{}\n",
352                output.status,
353                String::from_utf8_lossy(&output.stdout),
354                String::from_utf8_lossy(&output.stderr),
355            );
356        }
357        Ok(output)
358    }
359
360    /// Run the command ignoring its output.
361    pub async fn run(&mut self) -> Result<()> {
362        let _ = self
363            .run_inner(true)
364            .await
365            .with_context(|| format!("command: {}", self.command_debug()))?;
366        Ok(())
367    }
368
369    /// Run the command logging the output and error
370    pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
371        let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
372        let path = format!("{logs_dir}/{name}.log");
373        let log = OpenOptions::new()
374            .append(true)
375            .create(true)
376            .open(&path)
377            .await
378            .with_context(|| format!("path: {path} cmd: {name}"))?
379            .into_std()
380            .await;
381        self.cmd.stdout(log.try_clone()?);
382        self.cmd.stderr(log);
383        let status = self
384            .cmd
385            .spawn()
386            .with_context(|| format!("cmd: {name}"))?
387            .wait()
388            .await?;
389        if !status.success() {
390            bail!("{}", status);
391        }
392        Ok(())
393    }
394}
395
396/// easy syntax to create a Command
397///
398/// `(A1, A2, A3)` expands to
399/// ```ignore
400/// A1.cmd().await?
401///     .arg(A2)
402///     .arg(A3)
403///     .kill_on_drop(true)
404/// ```
405///
406/// If `An` is a string literal, it is replaced with `format!(a)`
407#[macro_export]
408macro_rules! cmd {
409    ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
410        cmd! {
411            @head ($($($head)*)? format!($curr),)
412            $($($tail)*)?
413        }
414    };
415    ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
416        cmd! {
417            @head ($($($head)*)? $curr,)
418            $($($tail)*)?
419        }
420    };
421    (@head ($($head:tt)* )) => {
422        cmd! {
423            @last
424            $($head)*
425        }
426    };
427    // last matcher
428    (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
429        {
430            #[allow(unused)]
431            use $crate::util::ToCmdExt;
432            $this.cmd()
433                $(.arg(&$arg))*
434                .kill_on_drop(true)
435                .env("RUST_BACKTRACE", "1")
436        }
437    };
438}
439
440#[macro_export]
441macro_rules! poll_eq {
442    ($left:expr_2021, $right:expr_2021) => {
443        match ($left, $right) {
444            (left, right) => {
445                if left == right {
446                    Ok(())
447                } else {
448                    Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
449                        "assertion failed, left: {left:?} right: {right:?}"
450                    )))
451                }
452            }
453        }
454    };
455}
456
457#[macro_export]
458macro_rules! poll_almost_equal {
459    ($left:expr_2021, $right:expr_2021) => {
460        match ($left, $right) {
461            (left, right) => $crate::util::almost_equal(left, right, 10_000)
462                .map_err(|e| std::ops::ControlFlow::Continue(anyhow::anyhow!(e))),
463        }
464    };
465}
466
467pub fn almost_equal(a: u64, b: u64, max: u64) -> Result<(), String> {
468    if a.abs_diff(b) <= max {
469        Ok(())
470    } else {
471        Err(format!(
472            "Expected difference is {max} but we found {}",
473            a.abs_diff(b)
474        ))
475    }
476}
477
478// Allow macro to be used within the crate. See https://stackoverflow.com/a/31749071.
479pub(crate) use cmd;
480
481/// Retry until `f` succeeds or timeout is reached
482///
483/// - if `f` return Ok(val), this returns with Ok(val).
484/// - if `f` return Err(Control::Break(err)), this returns Err(err)
485/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
486///   reached
487pub async fn poll_with_timeout<Fut, R>(
488    name: &str,
489    timeout: Duration,
490    f: impl Fn() -> Fut,
491) -> Result<R>
492where
493    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
494{
495    const MIN_BACKOFF: Duration = Duration::from_millis(50);
496    const MAX_BACKOFF: Duration = Duration::from_secs(1);
497
498    let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
499    let start = now();
500    for attempt in 0u64.. {
501        let attempt_start = now();
502        match f().await {
503            Ok(value) => return Ok(value),
504            Err(ControlFlow::Break(err)) => {
505                return Err(err).with_context(|| format!("polling {name}"));
506            }
507            Err(ControlFlow::Continue(err))
508                if attempt_start
509                    .duration_since(start)
510                    .expect("time goes forward")
511                    < timeout =>
512            {
513                debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
514                task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
515            }
516            Err(ControlFlow::Continue(err)) => {
517                return Err(err).with_context(|| {
518                    format!(
519                        "Polling {name} failed after {attempt} retries (timeout: {}s)",
520                        timeout.as_secs()
521                    )
522                });
523            }
524        }
525    }
526
527    unreachable!();
528}
529
530const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(60);
531const EXTRA_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(90);
532
533/// Retry until `f` succeeds or default timeout is reached
534///
535/// - if `f` return Ok(val), this returns with Ok(val).
536/// - if `f` return Err(Control::Break(err)), this returns Err(err)
537/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
538///   reached
539pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
540where
541    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
542{
543    poll_with_timeout(
544        name,
545        if is_env_var_set("FM_EXTRA_LONG_POLL") {
546            EXTRA_LONG_POLL_TIMEOUT
547        } else {
548            DEFAULT_POLL_TIMEOUT
549        },
550        f,
551    )
552    .await
553}
554
555pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
556where
557    Fut: Future<Output = Result<R, anyhow::Error>>,
558{
559    poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
560}
561
562// used to add `cmd` method.
563pub trait ToCmdExt {
564    fn cmd(self) -> Command;
565}
566
567// a command that uses self as program name
568impl ToCmdExt for &'_ str {
569    fn cmd(self) -> Command {
570        Command {
571            cmd: tokio::process::Command::new(self),
572            args_debug: vec![self.to_owned()],
573        }
574    }
575}
576
577impl ToCmdExt for Vec<String> {
578    fn cmd(self) -> Command {
579        to_command(self)
580    }
581}
582
583pub trait JsonValueExt {
584    fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
585}
586
587impl JsonValueExt for serde_json::Value {
588    fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
589        Ok(serde_json::from_value(self)?)
590    }
591}
592
593const GATEWAYD_FALLBACK: &str = "gatewayd";
594
595const FEDIMINTD_FALLBACK: &str = "fedimintd";
596
597const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
598
599pub fn get_fedimint_cli_path() -> Vec<String> {
600    get_command_str_for_alias(
601        &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
602        &[FEDIMINT_CLI_FALLBACK],
603    )
604}
605
606const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
607
608pub fn get_gateway_cli_path() -> Vec<String> {
609    get_command_str_for_alias(
610        &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
611        &[GATEWAY_CLI_FALLBACK],
612    )
613}
614
615const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
616
617const LNCLI_FALLBACK: &str = "lncli";
618
619pub fn get_lncli_path() -> Vec<String> {
620    get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
621}
622
623const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
624
625pub fn get_bitcoin_cli_path() -> Vec<String> {
626    get_command_str_for_alias(
627        &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
628        &[BITCOIN_CLI_FALLBACK],
629    )
630}
631
632const BITCOIND_FALLBACK: &str = "bitcoind";
633
634const LND_FALLBACK: &str = "lnd";
635
636const ESPLORA_FALLBACK: &str = "esplora";
637
638const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
639
640const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
641
642const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
643
644pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
645    get_command_str_for_alias(
646        &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
647        &[FEDIMINT_DBTOOL_FALLBACK],
648    )
649}
650
651/// Maps a version hash to a release version
652fn version_hash_to_version(version_hash: &str) -> Result<Version> {
653    match version_hash {
654        "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
655        "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
656        _ => Err(anyhow!("no version known for version hash: {version_hash}")),
657    }
658}
659
660pub struct FedimintdCmd;
661impl FedimintdCmd {
662    pub fn cmd(self) -> Command {
663        to_command(get_command_str_for_alias(
664            &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
665            &[FEDIMINTD_FALLBACK],
666        ))
667    }
668
669    /// Returns the fedimintd version from clap or default min version
670    pub async fn version_or_default() -> Version {
671        match cmd!(FedimintdCmd, "--version").out_string().await {
672            Ok(version) => parse_clap_version(&version),
673            Err(_) => cmd!(FedimintdCmd, "version-hash")
674                .out_string()
675                .await
676                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
677                .unwrap_or(DEFAULT_VERSION),
678        }
679    }
680}
681
682pub struct Gatewayd;
683impl Gatewayd {
684    pub fn cmd(self) -> Command {
685        to_command(get_command_str_for_alias(
686            &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
687            &[GATEWAYD_FALLBACK],
688        ))
689    }
690
691    /// Returns the gatewayd version from clap or default min version
692    pub async fn version_or_default() -> Version {
693        match cmd!(Gatewayd, "--version").out_string().await {
694            Ok(version) => parse_clap_version(&version),
695            Err(_) => cmd!(Gatewayd, "version-hash")
696                .out_string()
697                .await
698                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
699                .unwrap_or(DEFAULT_VERSION),
700        }
701    }
702}
703
704pub struct FedimintCli;
705impl FedimintCli {
706    pub fn cmd(self) -> Command {
707        to_command(get_command_str_for_alias(
708            &[FM_MINT_CLIENT_ENV],
709            &get_fedimint_cli_path()
710                .iter()
711                .map(String::as_str)
712                .collect::<Vec<_>>(),
713        ))
714    }
715
716    /// Returns the fedimint-cli version from clap or default min version
717    pub async fn version_or_default() -> Version {
718        match cmd!(FedimintCli, "--version").out_string().await {
719            Ok(version) => parse_clap_version(&version),
720            Err(_) => DEFAULT_VERSION,
721        }
722    }
723
724    pub async fn ws_status(self, endpoint: &str) -> Result<StatusResponse> {
725        let status = cmd!(self, "admin", "dkg", "--ws", endpoint, "ws-status")
726            .out_json()
727            .await?;
728        Ok(serde_json::from_value(status)?)
729    }
730
731    pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
732        cmd!(
733            self,
734            "--password",
735            &auth.0,
736            "admin",
737            "dkg",
738            "--ws",
739            endpoint,
740            "set-password",
741        )
742        .run()
743        .await
744    }
745
746    pub async fn set_local_params_leader(
747        self,
748        peer: &PeerId,
749        auth: &ApiAuth,
750        endpoint: &str,
751    ) -> Result<String> {
752        let json = cmd!(
753            self,
754            "--password",
755            &auth.0,
756            "admin",
757            "setup",
758            endpoint,
759            "set-local-params",
760            format!("Devimint Guardian {peer}"),
761            "--federation-name",
762            "Devimint Federation"
763        )
764        .out_json()
765        .await?;
766
767        Ok(serde_json::from_value(json)?)
768    }
769
770    pub async fn set_local_params_follower(
771        self,
772        peer: &PeerId,
773        auth: &ApiAuth,
774        endpoint: &str,
775    ) -> Result<String> {
776        let json = cmd!(
777            self,
778            "--password",
779            &auth.0,
780            "admin",
781            "setup",
782            endpoint,
783            "set-local-params",
784            format!("Devimint Guardian {peer}")
785        )
786        .out_json()
787        .await?;
788
789        Ok(serde_json::from_value(json)?)
790    }
791
792    pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
793        cmd!(
794            self,
795            "--password",
796            &auth.0,
797            "admin",
798            "setup",
799            endpoint,
800            "add-peer",
801            params
802        )
803        .run()
804        .await
805    }
806
807    pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
808        let json = cmd!(
809            self,
810            "--password",
811            &auth.0,
812            "admin",
813            "setup",
814            endpoint,
815            "status",
816        )
817        .out_json()
818        .await?;
819
820        Ok(serde_json::from_value(json)?)
821    }
822
823    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
824        cmd!(
825            self,
826            "--password",
827            &auth.0,
828            "admin",
829            "setup",
830            endpoint,
831            "start-dkg"
832        )
833        .run()
834        .await
835    }
836
837    pub async fn set_config_gen_params(
838        self,
839        auth: &ApiAuth,
840        endpoint: &str,
841        meta: BTreeMap<String, String>,
842        server_gen_params: ServerModuleConfigGenParamsRegistry,
843    ) -> Result<()> {
844        cmd!(
845            self,
846            "--password",
847            &auth.0,
848            "admin",
849            "dkg",
850            "--ws",
851            endpoint,
852            "set-config-gen-params",
853            "--meta-json",
854            serde_json::to_string(&meta)?,
855            "--modules-json",
856            serde_json::to_string(&server_gen_params)?
857        )
858        .run()
859        .await
860    }
861
862    pub async fn consensus_config_gen_params_legacy(
863        self,
864        endpoint: &str,
865    ) -> Result<ConfigGenParamsResponseLegacy> {
866        let result = cmd!(
867            self,
868            "admin",
869            "dkg",
870            "--ws",
871            endpoint,
872            "consensus-config-gen-params"
873        )
874        .out_json()
875        .await
876        .context("non-json returned for consensus_config_gen_params")?;
877        Ok(serde_json::from_value(result)?)
878    }
879
880    pub async fn set_config_gen_connections(
881        self,
882        auth: &ApiAuth,
883        endpoint: &str,
884        our_name: &str,
885        leader_api_url: Option<&str>,
886    ) -> Result<()> {
887        // FIXME: this should be a single command
888        if let Some(leader_api_url) = leader_api_url {
889            cmd!(
890                self,
891                "--password",
892                &auth.0,
893                "admin",
894                "dkg",
895                "--ws",
896                endpoint,
897                "set-config-gen-connections",
898                "--our-name",
899                our_name,
900                "--leader-api-url",
901                leader_api_url,
902            )
903            .run()
904            .await
905        } else {
906            cmd!(
907                self,
908                "--password",
909                &auth.0,
910                "admin",
911                "dkg",
912                "--ws",
913                endpoint,
914                "set-config-gen-connections",
915                "--our-name",
916                our_name,
917            )
918            .run()
919            .await
920        }
921    }
922
923    pub async fn get_config_gen_peers(self, endpoint: &str) -> Result<Vec<PeerServerParamsLegacy>> {
924        let result = cmd!(
925            self,
926            "admin",
927            "dkg",
928            "--ws",
929            endpoint,
930            "get-config-gen-peers"
931        )
932        .out_json()
933        .await
934        .context("non-json returned for get_config_gen_peers")?;
935        Ok(serde_json::from_value(result)?)
936    }
937
938    pub async fn run_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
939        cmd!(
940            self,
941            "--password",
942            &auth.0,
943            "admin",
944            "dkg",
945            "--ws",
946            endpoint,
947            "run-dkg"
948        )
949        .run()
950        .await
951    }
952
953    pub async fn get_verify_config_hash(
954        self,
955        auth: &ApiAuth,
956        endpoint: &str,
957    ) -> Result<BTreeMap<PeerId, bitcoincore_rpc::bitcoin::hashes::sha256::Hash>> {
958        let result = cmd!(
959            self,
960            "--password",
961            &auth.0,
962            "admin",
963            "dkg",
964            "--ws",
965            endpoint,
966            "get-verify-config-hash"
967        )
968        .out_json()
969        .await
970        .context("non-json returned for get_verify_config_hash")?;
971        Ok(serde_json::from_value(result)?)
972    }
973
974    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
975        cmd!(
976            self,
977            "--password",
978            &auth.0,
979            "--our-id",
980            our_id,
981            "admin",
982            "shutdown",
983            session_count,
984        )
985        .run()
986        .await
987    }
988
989    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
990        cmd!(
991            self,
992            "--password",
993            &auth.0,
994            "--our-id",
995            our_id,
996            "admin",
997            "status",
998        )
999        .run()
1000        .await
1001    }
1002
1003    pub async fn start_consensus(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
1004        cmd!(
1005            self,
1006            "--password",
1007            &auth.0,
1008            "admin",
1009            "dkg",
1010            "--ws",
1011            endpoint,
1012            "start-consensus"
1013        )
1014        .run()
1015        .await
1016    }
1017}
1018
1019pub struct LoadTestTool;
1020impl LoadTestTool {
1021    pub fn cmd(self) -> Command {
1022        to_command(get_command_str_for_alias(
1023            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
1024            &[LOAD_TEST_TOOL_FALLBACK],
1025        ))
1026    }
1027}
1028
1029pub struct GatewayCli;
1030impl GatewayCli {
1031    pub fn cmd(self) -> Command {
1032        to_command(get_command_str_for_alias(
1033            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
1034            &get_gateway_cli_path()
1035                .iter()
1036                .map(String::as_str)
1037                .collect::<Vec<_>>(),
1038        ))
1039    }
1040
1041    /// Returns the gateway-cli version from clap or default min version
1042    pub async fn version_or_default() -> Version {
1043        match cmd!(GatewayCli, "--version").out_string().await {
1044            Ok(version) => parse_clap_version(&version),
1045            Err(_) => DEFAULT_VERSION,
1046        }
1047    }
1048}
1049
1050pub struct GatewayLndCli;
1051impl GatewayLndCli {
1052    pub fn cmd(self) -> Command {
1053        to_command(get_command_str_for_alias(
1054            &[FM_GWCLI_LND_ENV],
1055            &["gateway-lnd"],
1056        ))
1057    }
1058}
1059
1060pub struct GatewayLdkCli;
1061impl GatewayLdkCli {
1062    pub fn cmd(self) -> Command {
1063        to_command(get_command_str_for_alias(
1064            &[FM_GWCLI_LDK_ENV],
1065            &["gateway-ldk"],
1066        ))
1067    }
1068}
1069
1070pub struct LnCli;
1071impl LnCli {
1072    pub fn cmd(self) -> Command {
1073        to_command(get_command_str_for_alias(
1074            &[FM_LNCLI_ENV],
1075            &get_lncli_path()
1076                .iter()
1077                .map(String::as_str)
1078                .collect::<Vec<_>>(),
1079        ))
1080    }
1081}
1082
1083pub struct BitcoinCli;
1084impl BitcoinCli {
1085    pub fn cmd(self) -> Command {
1086        to_command(get_command_str_for_alias(
1087            &[FM_BTC_CLIENT_ENV],
1088            &get_bitcoin_cli_path()
1089                .iter()
1090                .map(String::as_str)
1091                .collect::<Vec<_>>(),
1092        ))
1093    }
1094}
1095
1096pub struct Bitcoind;
1097impl Bitcoind {
1098    pub fn cmd(self) -> Command {
1099        to_command(get_command_str_for_alias(
1100            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
1101            &[BITCOIND_FALLBACK],
1102        ))
1103    }
1104}
1105
1106pub struct Lnd;
1107impl Lnd {
1108    pub fn cmd(self) -> Command {
1109        to_command(get_command_str_for_alias(
1110            &[FM_LND_BASE_EXECUTABLE_ENV],
1111            &[LND_FALLBACK],
1112        ))
1113    }
1114}
1115
1116pub struct Esplora;
1117impl Esplora {
1118    pub fn cmd(self) -> Command {
1119        to_command(get_command_str_for_alias(
1120            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
1121            &[ESPLORA_FALLBACK],
1122        ))
1123    }
1124}
1125
1126pub struct Recoverytool;
1127impl Recoverytool {
1128    pub fn cmd(self) -> Command {
1129        to_command(get_command_str_for_alias(
1130            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
1131            &[RECOVERYTOOL_FALLBACK],
1132        ))
1133    }
1134}
1135
1136pub struct DevimintFaucet;
1137impl DevimintFaucet {
1138    pub fn cmd(self) -> Command {
1139        to_command(get_command_str_for_alias(
1140            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
1141            &[DEVIMINT_FAUCET_FALLBACK],
1142        ))
1143    }
1144}
1145
1146pub struct Recurringd;
1147impl Recurringd {
1148    pub fn cmd(self) -> Command {
1149        to_command(get_command_str_for_alias(
1150            &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
1151            &["fedimint-recurringd"],
1152        ))
1153    }
1154}
1155
1156fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
1157    // try to use one of the aliases if set
1158    for alias in aliases {
1159        if let Ok(cmd) = std::env::var(alias) {
1160            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
1161        }
1162    }
1163    // otherwise return the default value
1164    default.iter().map(ToString::to_string).collect()
1165}
1166
1167fn to_command(cli: Vec<String>) -> Command {
1168    let mut cmd = tokio::process::Command::new(&cli[0]);
1169    cmd.args(&cli[1..]);
1170    Command {
1171        cmd,
1172        args_debug: cli,
1173    }
1174}
1175
1176pub fn supports_lnv2() -> bool {
1177    std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
1178        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1179}
1180
1181/// Returns true if running backwards-compatibility tests
1182pub fn is_backwards_compatibility_test() -> bool {
1183    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1184}
1185
1186/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1187/// needed for running DKG. Returns the original fedimint-cli path and mint
1188/// client alias so the caller can reset the fedimint-cli version after DKG
1189pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1190    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1191    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1192    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1193
1194    if pkg_version == fedimintd_version {
1195        // we're on the current version if the fedimintd version is the same as the
1196        // package version. to use the current version of `fedimint-cli` built by cargo,
1197        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1198        // TODO: Audit that the environment access only happens in single-threaded code.
1199        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1200    } else {
1201        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1202
1203        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1204        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1205        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1206        // TODO: Audit that the environment access only happens in single-threaded code.
1207        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1208    }
1209
1210    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1211    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1212    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1213
1214    let fm_mint_client: String = format!(
1215        "{fedimint_cli} --data-dir {datadir}",
1216        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1217        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1218    );
1219    // TODO: Audit that the environment access only happens in single-threaded code.
1220    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1221
1222    Ok((original_fedimint_cli_path, original_fm_mint_client))
1223}
1224
1225/// Sets the fedimint-cli and mint client alias
1226pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1227    // TODO: Audit that the environment access only happens in single-threaded code.
1228    unsafe {
1229        std::env::set_var(
1230            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1231            original_fedimint_cli_path,
1232        );
1233    };
1234
1235    // TODO: Audit that the environment access only happens in single-threaded code.
1236    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1237}
1238
1239/// Parses a version string returned from clap
1240/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1241fn parse_clap_version(res: &str) -> Version {
1242    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1243        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1244        _ => DEFAULT_VERSION,
1245    }
1246}
1247
1248#[test]
1249fn test_parse_clap_version() -> Result<()> {
1250    let version_str = "fedimintd 0.3.0-alpha";
1251    let expected_version = Version::parse("0.3.0-alpha")?;
1252    assert_eq!(expected_version, parse_clap_version(version_str));
1253
1254    let version_str = "fedimintd 0.3.12";
1255    let expected_version = Version::parse("0.3.12")?;
1256    assert_eq!(expected_version, parse_clap_version(version_str));
1257
1258    let version_str = "fedimint-cli 2.12.2-rc22";
1259    let expected_version = Version::parse("2.12.2-rc22")?;
1260    assert_eq!(expected_version, parse_clap_version(version_str));
1261
1262    let version_str = "bad version";
1263    let expected_version = DEFAULT_VERSION;
1264    assert_eq!(expected_version, parse_clap_version(version_str));
1265
1266    Ok(())
1267}
1268
1269mod legacy_types {
1270    use std::collections::BTreeMap;
1271
1272    use fedimint_core::PeerId;
1273    use fedimint_core::admin_client::PeerServerParamsLegacy;
1274    use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
1275    use serde::{Deserialize, Serialize};
1276
1277    /// The config gen params that need to be in consensus, sent by the config
1278    /// gen leader to all the other guardians
1279    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1280    pub struct ConfigGenParamsConsensusLegacy {
1281        /// Endpoints of all servers
1282        pub peers: BTreeMap<PeerId, PeerServerParamsLegacy>,
1283        /// Guardian-defined key-value pairs that will be passed to the client
1284        pub meta: BTreeMap<String, String>,
1285        /// Module init params (also contains local params from us)
1286        pub modules: ServerModuleConfigGenParamsRegistry,
1287    }
1288
1289    /// The config gen params response which includes our peer id
1290    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1291    pub struct ConfigGenParamsResponseLegacy {
1292        /// The same for all peers
1293        pub consensus: ConfigGenParamsConsensusLegacy,
1294        /// Our id (might change if new peers join)
1295        pub our_current_id: PeerId,
1296    }
1297}