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