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);
531
532/// Retry until `f` succeeds or default timeout is reached
533///
534/// - if `f` return Ok(val), this returns with Ok(val).
535/// - if `f` return Err(Control::Break(err)), this returns Err(err)
536/// - if `f` return Err(ControlFlow::Continue(err)), retries until timeout
537///   reached
538pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
539where
540    Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
541{
542    poll_with_timeout(name, DEFAULT_POLL_TIMEOUT, f).await
543}
544
545pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
546where
547    Fut: Future<Output = Result<R, anyhow::Error>>,
548{
549    poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
550}
551
552// used to add `cmd` method.
553pub trait ToCmdExt {
554    fn cmd(self) -> Command;
555}
556
557// a command that uses self as program name
558impl ToCmdExt for &'_ str {
559    fn cmd(self) -> Command {
560        Command {
561            cmd: tokio::process::Command::new(self),
562            args_debug: vec![self.to_owned()],
563        }
564    }
565}
566
567impl ToCmdExt for Vec<String> {
568    fn cmd(self) -> Command {
569        to_command(self)
570    }
571}
572
573pub trait JsonValueExt {
574    fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
575}
576
577impl JsonValueExt for serde_json::Value {
578    fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
579        Ok(serde_json::from_value(self)?)
580    }
581}
582
583const GATEWAYD_FALLBACK: &str = "gatewayd";
584
585const FEDIMINTD_FALLBACK: &str = "fedimintd";
586
587const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
588
589pub fn get_fedimint_cli_path() -> Vec<String> {
590    get_command_str_for_alias(
591        &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
592        &[FEDIMINT_CLI_FALLBACK],
593    )
594}
595
596const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
597
598pub fn get_gateway_cli_path() -> Vec<String> {
599    get_command_str_for_alias(
600        &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
601        &[GATEWAY_CLI_FALLBACK],
602    )
603}
604
605const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
606
607const LNCLI_FALLBACK: &str = "lncli";
608
609pub fn get_lncli_path() -> Vec<String> {
610    get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
611}
612
613const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
614
615pub fn get_bitcoin_cli_path() -> Vec<String> {
616    get_command_str_for_alias(
617        &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
618        &[BITCOIN_CLI_FALLBACK],
619    )
620}
621
622const BITCOIND_FALLBACK: &str = "bitcoind";
623
624const LND_FALLBACK: &str = "lnd";
625
626const ESPLORA_FALLBACK: &str = "esplora";
627
628const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
629
630const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
631
632const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
633
634pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
635    get_command_str_for_alias(
636        &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
637        &[FEDIMINT_DBTOOL_FALLBACK],
638    )
639}
640
641/// Maps a version hash to a release version
642fn version_hash_to_version(version_hash: &str) -> Result<Version> {
643    match version_hash {
644        "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
645        "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
646        _ => Err(anyhow!("no version known for version hash: {version_hash}")),
647    }
648}
649
650pub struct FedimintdCmd;
651impl FedimintdCmd {
652    pub fn cmd(self) -> Command {
653        to_command(get_command_str_for_alias(
654            &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
655            &[FEDIMINTD_FALLBACK],
656        ))
657    }
658
659    /// Returns the fedimintd version from clap or default min version
660    pub async fn version_or_default() -> Version {
661        match cmd!(FedimintdCmd, "--version").out_string().await {
662            Ok(version) => parse_clap_version(&version),
663            Err(_) => cmd!(FedimintdCmd, "version-hash")
664                .out_string()
665                .await
666                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
667                .unwrap_or(DEFAULT_VERSION),
668        }
669    }
670}
671
672pub struct Gatewayd;
673impl Gatewayd {
674    pub fn cmd(self) -> Command {
675        to_command(get_command_str_for_alias(
676            &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
677            &[GATEWAYD_FALLBACK],
678        ))
679    }
680
681    /// Returns the gatewayd version from clap or default min version
682    pub async fn version_or_default() -> Version {
683        match cmd!(Gatewayd, "--version").out_string().await {
684            Ok(version) => parse_clap_version(&version),
685            Err(_) => cmd!(Gatewayd, "version-hash")
686                .out_string()
687                .await
688                .map(|v| version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION))
689                .unwrap_or(DEFAULT_VERSION),
690        }
691    }
692}
693
694pub struct FedimintCli;
695impl FedimintCli {
696    pub fn cmd(self) -> Command {
697        to_command(get_command_str_for_alias(
698            &[FM_MINT_CLIENT_ENV],
699            &get_fedimint_cli_path()
700                .iter()
701                .map(String::as_str)
702                .collect::<Vec<_>>(),
703        ))
704    }
705
706    /// Returns the fedimint-cli version from clap or default min version
707    pub async fn version_or_default() -> Version {
708        match cmd!(FedimintCli, "--version").out_string().await {
709            Ok(version) => parse_clap_version(&version),
710            Err(_) => DEFAULT_VERSION,
711        }
712    }
713
714    pub async fn ws_status(self, endpoint: &str) -> Result<StatusResponse> {
715        let status = cmd!(self, "admin", "dkg", "--ws", endpoint, "ws-status")
716            .out_json()
717            .await?;
718        Ok(serde_json::from_value(status)?)
719    }
720
721    pub async fn set_password(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
722        cmd!(
723            self,
724            "--password",
725            &auth.0,
726            "admin",
727            "dkg",
728            "--ws",
729            endpoint,
730            "set-password",
731        )
732        .run()
733        .await
734    }
735
736    pub async fn set_local_params_leader(
737        self,
738        peer: &PeerId,
739        auth: &ApiAuth,
740        endpoint: &str,
741    ) -> Result<String> {
742        let json = cmd!(
743            self,
744            "--password",
745            &auth.0,
746            "admin",
747            "setup",
748            endpoint,
749            "set-local-params",
750            format!("Devimint Guardian {peer}"),
751            "--federation-name",
752            "Devimint Federation"
753        )
754        .out_json()
755        .await?;
756
757        Ok(serde_json::from_value(json)?)
758    }
759
760    pub async fn set_local_params_follower(
761        self,
762        peer: &PeerId,
763        auth: &ApiAuth,
764        endpoint: &str,
765    ) -> Result<String> {
766        let json = cmd!(
767            self,
768            "--password",
769            &auth.0,
770            "admin",
771            "setup",
772            endpoint,
773            "set-local-params",
774            format!("Devimint Guardian {peer}")
775        )
776        .out_json()
777        .await?;
778
779        Ok(serde_json::from_value(json)?)
780    }
781
782    pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
783        cmd!(
784            self,
785            "--password",
786            &auth.0,
787            "admin",
788            "setup",
789            endpoint,
790            "add-peer",
791            params
792        )
793        .run()
794        .await
795    }
796
797    pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
798        let json = cmd!(
799            self,
800            "--password",
801            &auth.0,
802            "admin",
803            "setup",
804            endpoint,
805            "status",
806        )
807        .out_json()
808        .await?;
809
810        Ok(serde_json::from_value(json)?)
811    }
812
813    pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
814        cmd!(
815            self,
816            "--password",
817            &auth.0,
818            "admin",
819            "setup",
820            endpoint,
821            "start-dkg"
822        )
823        .run()
824        .await
825    }
826
827    pub async fn set_config_gen_params(
828        self,
829        auth: &ApiAuth,
830        endpoint: &str,
831        meta: BTreeMap<String, String>,
832        server_gen_params: ServerModuleConfigGenParamsRegistry,
833    ) -> Result<()> {
834        cmd!(
835            self,
836            "--password",
837            &auth.0,
838            "admin",
839            "dkg",
840            "--ws",
841            endpoint,
842            "set-config-gen-params",
843            "--meta-json",
844            serde_json::to_string(&meta)?,
845            "--modules-json",
846            serde_json::to_string(&server_gen_params)?
847        )
848        .run()
849        .await
850    }
851
852    pub async fn consensus_config_gen_params_legacy(
853        self,
854        endpoint: &str,
855    ) -> Result<ConfigGenParamsResponseLegacy> {
856        let result = cmd!(
857            self,
858            "admin",
859            "dkg",
860            "--ws",
861            endpoint,
862            "consensus-config-gen-params"
863        )
864        .out_json()
865        .await
866        .context("non-json returned for consensus_config_gen_params")?;
867        Ok(serde_json::from_value(result)?)
868    }
869
870    pub async fn set_config_gen_connections(
871        self,
872        auth: &ApiAuth,
873        endpoint: &str,
874        our_name: &str,
875        leader_api_url: Option<&str>,
876    ) -> Result<()> {
877        // FIXME: this should be a single command
878        if let Some(leader_api_url) = leader_api_url {
879            cmd!(
880                self,
881                "--password",
882                &auth.0,
883                "admin",
884                "dkg",
885                "--ws",
886                endpoint,
887                "set-config-gen-connections",
888                "--our-name",
889                our_name,
890                "--leader-api-url",
891                leader_api_url,
892            )
893            .run()
894            .await
895        } else {
896            cmd!(
897                self,
898                "--password",
899                &auth.0,
900                "admin",
901                "dkg",
902                "--ws",
903                endpoint,
904                "set-config-gen-connections",
905                "--our-name",
906                our_name,
907            )
908            .run()
909            .await
910        }
911    }
912
913    pub async fn get_config_gen_peers(self, endpoint: &str) -> Result<Vec<PeerServerParamsLegacy>> {
914        let result = cmd!(
915            self,
916            "admin",
917            "dkg",
918            "--ws",
919            endpoint,
920            "get-config-gen-peers"
921        )
922        .out_json()
923        .await
924        .context("non-json returned for get_config_gen_peers")?;
925        Ok(serde_json::from_value(result)?)
926    }
927
928    pub async fn run_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
929        cmd!(
930            self,
931            "--password",
932            &auth.0,
933            "admin",
934            "dkg",
935            "--ws",
936            endpoint,
937            "run-dkg"
938        )
939        .run()
940        .await
941    }
942
943    pub async fn get_verify_config_hash(
944        self,
945        auth: &ApiAuth,
946        endpoint: &str,
947    ) -> Result<BTreeMap<PeerId, bitcoincore_rpc::bitcoin::hashes::sha256::Hash>> {
948        let result = cmd!(
949            self,
950            "--password",
951            &auth.0,
952            "admin",
953            "dkg",
954            "--ws",
955            endpoint,
956            "get-verify-config-hash"
957        )
958        .out_json()
959        .await
960        .context("non-json returned for get_verify_config_hash")?;
961        Ok(serde_json::from_value(result)?)
962    }
963
964    pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
965        cmd!(
966            self,
967            "--password",
968            &auth.0,
969            "--our-id",
970            our_id,
971            "admin",
972            "shutdown",
973            session_count,
974        )
975        .run()
976        .await
977    }
978
979    pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
980        cmd!(
981            self,
982            "--password",
983            &auth.0,
984            "--our-id",
985            our_id,
986            "admin",
987            "status",
988        )
989        .run()
990        .await
991    }
992
993    pub async fn start_consensus(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
994        cmd!(
995            self,
996            "--password",
997            &auth.0,
998            "admin",
999            "dkg",
1000            "--ws",
1001            endpoint,
1002            "start-consensus"
1003        )
1004        .run()
1005        .await
1006    }
1007}
1008
1009pub struct LoadTestTool;
1010impl LoadTestTool {
1011    pub fn cmd(self) -> Command {
1012        to_command(get_command_str_for_alias(
1013            &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
1014            &[LOAD_TEST_TOOL_FALLBACK],
1015        ))
1016    }
1017}
1018
1019pub struct GatewayCli;
1020impl GatewayCli {
1021    pub fn cmd(self) -> Command {
1022        to_command(get_command_str_for_alias(
1023            &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
1024            &get_gateway_cli_path()
1025                .iter()
1026                .map(String::as_str)
1027                .collect::<Vec<_>>(),
1028        ))
1029    }
1030
1031    /// Returns the gateway-cli version from clap or default min version
1032    pub async fn version_or_default() -> Version {
1033        match cmd!(GatewayCli, "--version").out_string().await {
1034            Ok(version) => parse_clap_version(&version),
1035            Err(_) => DEFAULT_VERSION,
1036        }
1037    }
1038}
1039
1040pub struct GatewayLndCli;
1041impl GatewayLndCli {
1042    pub fn cmd(self) -> Command {
1043        to_command(get_command_str_for_alias(
1044            &[FM_GWCLI_LND_ENV],
1045            &["gateway-lnd"],
1046        ))
1047    }
1048}
1049
1050pub struct GatewayLdkCli;
1051impl GatewayLdkCli {
1052    pub fn cmd(self) -> Command {
1053        to_command(get_command_str_for_alias(
1054            &[FM_GWCLI_LDK_ENV],
1055            &["gateway-ldk"],
1056        ))
1057    }
1058}
1059
1060pub struct LnCli;
1061impl LnCli {
1062    pub fn cmd(self) -> Command {
1063        to_command(get_command_str_for_alias(
1064            &[FM_LNCLI_ENV],
1065            &get_lncli_path()
1066                .iter()
1067                .map(String::as_str)
1068                .collect::<Vec<_>>(),
1069        ))
1070    }
1071}
1072
1073pub struct BitcoinCli;
1074impl BitcoinCli {
1075    pub fn cmd(self) -> Command {
1076        to_command(get_command_str_for_alias(
1077            &[FM_BTC_CLIENT_ENV],
1078            &get_bitcoin_cli_path()
1079                .iter()
1080                .map(String::as_str)
1081                .collect::<Vec<_>>(),
1082        ))
1083    }
1084}
1085
1086pub struct Bitcoind;
1087impl Bitcoind {
1088    pub fn cmd(self) -> Command {
1089        to_command(get_command_str_for_alias(
1090            &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
1091            &[BITCOIND_FALLBACK],
1092        ))
1093    }
1094}
1095
1096pub struct Lnd;
1097impl Lnd {
1098    pub fn cmd(self) -> Command {
1099        to_command(get_command_str_for_alias(
1100            &[FM_LND_BASE_EXECUTABLE_ENV],
1101            &[LND_FALLBACK],
1102        ))
1103    }
1104}
1105
1106pub struct Esplora;
1107impl Esplora {
1108    pub fn cmd(self) -> Command {
1109        to_command(get_command_str_for_alias(
1110            &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
1111            &[ESPLORA_FALLBACK],
1112        ))
1113    }
1114}
1115
1116pub struct Recoverytool;
1117impl Recoverytool {
1118    pub fn cmd(self) -> Command {
1119        to_command(get_command_str_for_alias(
1120            &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
1121            &[RECOVERYTOOL_FALLBACK],
1122        ))
1123    }
1124}
1125
1126pub struct DevimintFaucet;
1127impl DevimintFaucet {
1128    pub fn cmd(self) -> Command {
1129        to_command(get_command_str_for_alias(
1130            &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
1131            &[DEVIMINT_FAUCET_FALLBACK],
1132        ))
1133    }
1134}
1135
1136pub struct Recurringd;
1137impl Recurringd {
1138    pub fn cmd(self) -> Command {
1139        to_command(get_command_str_for_alias(
1140            &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
1141            &["fedimint-recurringd"],
1142        ))
1143    }
1144}
1145
1146fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
1147    // try to use one of the aliases if set
1148    for alias in aliases {
1149        if let Ok(cmd) = std::env::var(alias) {
1150            return cmd.split_whitespace().map(ToOwned::to_owned).collect();
1151        }
1152    }
1153    // otherwise return the default value
1154    default.iter().map(ToString::to_string).collect()
1155}
1156
1157fn to_command(cli: Vec<String>) -> Command {
1158    let mut cmd = tokio::process::Command::new(&cli[0]);
1159    cmd.args(&cli[1..]);
1160    Command {
1161        cmd,
1162        args_debug: cli,
1163    }
1164}
1165
1166pub fn supports_lnv2() -> bool {
1167    std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
1168        || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
1169}
1170
1171/// Returns true if running backwards-compatibility tests
1172pub fn is_backwards_compatibility_test() -> bool {
1173    is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
1174}
1175
1176/// Sets the fedimint-cli binary to match the fedimintd's version, which is
1177/// needed for running DKG. Returns the original fedimint-cli path and mint
1178/// client alias so the caller can reset the fedimint-cli version after DKG
1179pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1180    let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1181    let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1182    let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1183
1184    if pkg_version == fedimintd_version {
1185        // we're on the current version if the fedimintd version is the same as the
1186        // package version. to use the current version of `fedimint-cli` built by cargo,
1187        // we need to unset FM_FEDIMINT_CLI_BASE_EXECUTABLE
1188        // TODO: Audit that the environment access only happens in single-threaded code.
1189        unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1190    } else {
1191        let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1192
1193        // matches format defined by nix_binary_version_var_name in scripts/_common.sh
1194        let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1195        let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1196        // TODO: Audit that the environment access only happens in single-threaded code.
1197        unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1198    }
1199
1200    let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1201    let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1202    let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1203
1204    let fm_mint_client: String = format!(
1205        "{fedimint_cli} --data-dir {datadir}",
1206        fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1207        datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1208    );
1209    // TODO: Audit that the environment access only happens in single-threaded code.
1210    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1211
1212    Ok((original_fedimint_cli_path, original_fm_mint_client))
1213}
1214
1215/// Sets the fedimint-cli and mint client alias
1216pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1217    // TODO: Audit that the environment access only happens in single-threaded code.
1218    unsafe {
1219        std::env::set_var(
1220            FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1221            original_fedimint_cli_path,
1222        );
1223    };
1224
1225    // TODO: Audit that the environment access only happens in single-threaded code.
1226    unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1227}
1228
1229/// Parses a version string returned from clap
1230/// ex: fedimintd 0.3.0-alpha -> 0.3.0-alpha
1231fn parse_clap_version(res: &str) -> Version {
1232    match res.split(' ').collect::<Vec<&str>>().as_slice() {
1233        [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1234        _ => DEFAULT_VERSION,
1235    }
1236}
1237
1238#[test]
1239fn test_parse_clap_version() -> Result<()> {
1240    let version_str = "fedimintd 0.3.0-alpha";
1241    let expected_version = Version::parse("0.3.0-alpha")?;
1242    assert_eq!(expected_version, parse_clap_version(version_str));
1243
1244    let version_str = "fedimintd 0.3.12";
1245    let expected_version = Version::parse("0.3.12")?;
1246    assert_eq!(expected_version, parse_clap_version(version_str));
1247
1248    let version_str = "fedimint-cli 2.12.2-rc22";
1249    let expected_version = Version::parse("2.12.2-rc22")?;
1250    assert_eq!(expected_version, parse_clap_version(version_str));
1251
1252    let version_str = "bad version";
1253    let expected_version = DEFAULT_VERSION;
1254    assert_eq!(expected_version, parse_clap_version(version_str));
1255
1256    Ok(())
1257}
1258
1259mod legacy_types {
1260    use std::collections::BTreeMap;
1261
1262    use fedimint_core::PeerId;
1263    use fedimint_core::admin_client::PeerServerParamsLegacy;
1264    use fedimint_core::config::ServerModuleConfigGenParamsRegistry;
1265    use serde::{Deserialize, Serialize};
1266
1267    /// The config gen params that need to be in consensus, sent by the config
1268    /// gen leader to all the other guardians
1269    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1270    pub struct ConfigGenParamsConsensusLegacy {
1271        /// Endpoints of all servers
1272        pub peers: BTreeMap<PeerId, PeerServerParamsLegacy>,
1273        /// Guardian-defined key-value pairs that will be passed to the client
1274        pub meta: BTreeMap<String, String>,
1275        /// Module init params (also contains local params from us)
1276        pub modules: ServerModuleConfigGenParamsRegistry,
1277    }
1278
1279    /// The config gen params response which includes our peer id
1280    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1281    pub struct ConfigGenParamsResponseLegacy {
1282        /// The same for all peers
1283        pub consensus: ConfigGenParamsConsensusLegacy,
1284        /// Our id (might change if new peers join)
1285        pub our_current_id: PeerId,
1286    }
1287}