1use std::collections::BTreeMap;
2use std::ffi::OsStr;
3use std::future::Future;
4use std::ops::ControlFlow;
5use std::path::PathBuf;
6use std::process::Stdio;
7use std::sync::Arc;
8use std::time::Duration;
9use std::{env, unreachable};
10
11use anyhow::{Context, Result, anyhow, bail, format_err};
12use fedimint_core::PeerId;
13use fedimint_core::admin_client::SetupStatus;
14use fedimint_core::envs::{
15 FM_ENABLE_MODULE_LNV2_ENV, FM_ENABLE_MODULE_MINTV2_ENV, FM_ENABLE_MODULE_WALLETV2_ENV,
16 is_env_var_set,
17};
18use fedimint_core::module::ApiAuth;
19use fedimint_core::task::{self};
20use fedimint_core::time::now;
21use fedimint_core::util::FmtCompactAnyhow as _;
22use fedimint_core::util::backoff_util::custom_backoff;
23use fedimint_logging::LOG_DEVIMINT;
24use semver::Version;
25use serde::de::DeserializeOwned;
26use tokio::fs::OpenOptions;
27use tokio::process::Child;
28use tokio::sync::Mutex;
29use tracing::debug;
30
31use crate::envs::{
32 FM_BACKWARDS_COMPATIBILITY_TEST_ENV, FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV,
33 FM_BITCOIND_BASE_EXECUTABLE_ENV, FM_BTC_CLIENT_ENV, FM_CLIENT_DIR_ENV,
34 FM_DEVIMINT_CMD_INHERIT_STDERR_ENV, FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV,
35 FM_ESPLORA_BASE_EXECUTABLE_ENV, FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
36 FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV, FM_FEDIMINTD_BASE_EXECUTABLE_ENV,
37 FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV, FM_GATEWAYD_BASE_EXECUTABLE_ENV, FM_GWCLI_LDK_ENV,
38 FM_GWCLI_LND_ENV, FM_LNCLI_BASE_EXECUTABLE_ENV, FM_LNCLI_ENV, FM_LND_BASE_EXECUTABLE_ENV,
39 FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV, FM_LOGS_DIR_ENV, FM_MINT_CLIENT_ENV,
40 FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV, FM_RECURRINGD_BASE_EXECUTABLE_ENV,
41};
42
43const DEFAULT_VERSION: Version = Version::new(0, 2, 1);
46
47pub fn parse_map(s: &str) -> Result<BTreeMap<String, String>> {
48 let mut map = BTreeMap::new();
49
50 if s.is_empty() {
51 return Ok(map);
52 }
53
54 for pair in s.split(',') {
55 let parts: Vec<&str> = pair.split('=').collect();
56 if parts.len() == 2 {
57 map.insert(parts[0].to_string(), parts[1].to_string());
58 } else {
59 return Err(format_err!("Invalid pair in map: {pair}"));
60 }
61 }
62 Ok(map)
63}
64
65use crate::process_reaper;
66
67#[derive(Debug, Clone)]
72pub struct ProcessHandle(Arc<Mutex<ProcessHandleInner>>);
73
74impl ProcessHandle {
75 pub async fn terminate(&self) -> Result<()> {
76 let mut inner = self.0.lock().await;
77 inner.terminate();
78 Ok(())
79 }
80
81 pub async fn await_terminated(&self) -> Result<()> {
82 let mut inner = self.0.lock().await;
83 inner.await_terminated().await?;
84 Ok(())
85 }
86
87 pub async fn is_running(&self) -> bool {
88 self.0.lock().await.child.is_some()
89 }
90
91 pub async fn id(&self) -> Option<u32> {
92 self.0.lock().await.child.as_ref().and_then(Child::id)
93 }
94}
95
96#[derive(Debug)]
97pub struct ProcessHandleInner {
98 name: String,
99 child: Option<Child>,
100}
101
102impl ProcessHandleInner {
103 fn terminate(&mut self) {
115 if let Some(child) = self.child.take() {
116 debug!(
117 target: LOG_DEVIMINT,
118 name = %self.name,
119 "killing child process"
120 );
121
122 process_reaper::kill_process(&child);
123 process_reaper::reap_killed_processes();
124 }
125 }
126
127 async fn await_terminated(&mut self) -> anyhow::Result<()> {
128 match self
129 .child
130 .as_mut()
131 .expect("Process not running")
132 .wait()
133 .await
134 {
135 Ok(_status) => {
136 debug!(
137 target: LOG_DEVIMINT,
138 name=%self.name,
139 "child process terminated"
140 );
141 }
142 Err(err) => {
143 bail!("Failed to wait for child process {}: {}", self.name, err);
144 }
145 }
146
147 self.child.take();
149 Ok(())
150 }
151}
152
153impl Drop for ProcessHandleInner {
154 fn drop(&mut self) {
155 if self.child.is_none() {
156 return;
157 }
158
159 self.terminate();
160 }
161}
162
163#[derive(Clone)]
164pub struct ProcessManager {
165 pub globals: super::vars::Global,
166}
167
168impl ProcessManager {
169 pub fn new(globals: super::vars::Global) -> Self {
170 Self { globals }
171 }
172
173 pub async fn spawn_daemon(&self, name: &str, mut cmd: Command) -> Result<ProcessHandle> {
175 process_reaper::reap_killed_processes();
178 debug!(target: LOG_DEVIMINT, %name, "Spawning daemon");
179 let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
180 let path = format!("{logs_dir}/{name}.log");
181 let log = OpenOptions::new()
182 .append(true)
183 .create(true)
184 .open(path)
185 .await?
186 .into_std()
187 .await;
188 cmd.cmd.kill_on_drop(false); cmd.cmd.stdout(log.try_clone()?);
190 cmd.cmd.stderr(log);
191 let child = cmd
192 .cmd
193 .spawn()
194 .with_context(|| format!("Could not spawn: {name}"))?;
195 let handle = ProcessHandle(Arc::new(Mutex::new(ProcessHandleInner {
196 name: name.to_owned(),
197 child: Some(child),
198 })));
199 Ok(handle)
200 }
201}
202
203pub struct Command {
204 pub cmd: tokio::process::Command,
205 pub args_debug: Vec<String>,
206}
207
208impl Command {
209 pub fn arg<T: ToString>(mut self, arg: &T) -> Self {
210 let string = arg.to_string();
211 self.cmd.arg(string.clone());
212 self.args_debug.push(string);
213 self
214 }
215
216 pub fn args<T: ToString>(mut self, args: impl IntoIterator<Item = T>) -> Self {
217 for arg in args {
218 self = self.arg(&arg);
219 }
220 self
221 }
222
223 pub fn env<K, V>(mut self, key: K, val: V) -> Self
224 where
225 K: AsRef<OsStr>,
226 V: AsRef<OsStr>,
227 {
228 self.cmd.env(key, val);
229 self
230 }
231
232 pub fn envs<I, K, V>(mut self, env: I) -> Self
233 where
234 I: IntoIterator<Item = (K, V)>,
235 K: AsRef<OsStr>,
236 V: AsRef<OsStr>,
237 {
238 self.cmd.envs(env);
239 self
240 }
241
242 pub fn kill_on_drop(mut self, kill: bool) -> Self {
243 self.cmd.kill_on_drop(kill);
244 self
245 }
246
247 pub async fn out_json(&mut self) -> Result<serde_json::Value> {
249 Ok(serde_json::from_str(&self.out_string().await?)?)
250 }
251
252 fn command_debug(&self) -> String {
253 self.args_debug
254 .iter()
255 .map(|x| x.replace(' ', "␣"))
256 .collect::<Vec<_>>()
257 .join(" ")
258 }
259
260 pub async fn out_string(&mut self) -> Result<String> {
262 let output = self
263 .run_inner(true)
264 .await
265 .with_context(|| format!("command: {}", self.command_debug()))?;
266 let output = String::from_utf8(output.stdout)?;
267 Ok(output.trim().to_owned())
268 }
269
270 pub async fn expect_err_json(&mut self) -> Result<serde_json::Value> {
272 let output = self
273 .run_inner(false)
274 .await
275 .with_context(|| format!("command: {}", self.command_debug()))?;
276 let output = String::from_utf8(output.stdout)?;
277 Ok(serde_json::from_str(output.trim())?)
278 }
279
280 pub async fn assert_error(
283 &mut self,
284 predicate: impl Fn(serde_json::Value) -> bool,
285 ) -> Result<()> {
286 let parsed_error = self.expect_err_json().await?;
287 anyhow::ensure!(predicate(parsed_error));
288 Ok(())
289 }
290
291 pub async fn assert_error_contains(&mut self, error: &str) -> Result<()> {
294 self.assert_error(|err_json| {
295 let error_string = err_json
296 .get("error")
297 .expect("json error contains error field")
298 .as_str()
299 .expect("not a string")
300 .to_owned();
301
302 error_string.contains(error)
303 })
304 .await
305 }
306
307 pub async fn run_inner(&mut self, expect_success: bool) -> Result<std::process::Output> {
308 debug!(target: LOG_DEVIMINT, "> {}", self.command_debug());
309 let output = self
310 .cmd
311 .stdout(Stdio::piped())
312 .stderr(if is_env_var_set(FM_DEVIMINT_CMD_INHERIT_STDERR_ENV) {
313 Stdio::inherit()
314 } else {
315 Stdio::piped()
316 })
317 .spawn()?
318 .wait_with_output()
319 .await?;
320
321 if output.status.success() != expect_success {
322 bail!(
323 "{}\nstdout:\n{}\nstderr:\n{}\n",
324 output.status,
325 String::from_utf8_lossy(&output.stdout),
326 String::from_utf8_lossy(&output.stderr),
327 );
328 }
329 Ok(output)
330 }
331
332 pub async fn run(&mut self) -> Result<()> {
334 let _ = self
335 .run_inner(true)
336 .await
337 .with_context(|| format!("command: {}", self.command_debug()))?;
338 Ok(())
339 }
340
341 pub async fn run_with_logging(&mut self, name: String) -> Result<()> {
343 let logs_dir = env::var(FM_LOGS_DIR_ENV)?;
344 let path = format!("{logs_dir}/{name}.log");
345 let log = OpenOptions::new()
346 .append(true)
347 .create(true)
348 .open(&path)
349 .await
350 .with_context(|| format!("path: {path} cmd: {name}"))?
351 .into_std()
352 .await;
353 self.cmd.stdout(log.try_clone()?);
354 self.cmd.stderr(log);
355 let status = self
356 .cmd
357 .spawn()
358 .with_context(|| format!("cmd: {name}"))?
359 .wait()
360 .await?;
361 if !status.success() {
362 bail!("{status}");
363 }
364 Ok(())
365 }
366}
367
368#[macro_export]
380macro_rules! cmd {
381 ($(@head ($($head:tt)* ))? $curr:literal $(, $($tail:tt)*)?) => {
382 cmd! {
383 @head ($($($head)*)? format!($curr),)
384 $($($tail)*)?
385 }
386 };
387 ($(@head ($($head:tt)* ))? $curr:expr_2021 $(, $($tail:tt)*)?) => {
388 cmd! {
389 @head ($($($head)*)? $curr,)
390 $($($tail)*)?
391 }
392 };
393 (@head ($($head:tt)* )) => {
394 cmd! {
395 @last
396 $($head)*
397 }
398 };
399 (@last $this:expr_2021, $($arg:expr_2021),* $(,)?) => {
401 {
402 #[allow(unused)]
403 use $crate::util::ToCmdExt;
404 $this.cmd()
405 $(.arg(&$arg))*
406 .kill_on_drop(true)
407 .env("RUST_BACKTRACE", "1")
408 .env("RUST_LIB_BACKTRACE", "0")
409 }
410 };
411}
412
413#[macro_export]
414macro_rules! poll_eq {
415 ($left:expr_2021, $right:expr_2021) => {
416 match ($left, $right) {
417 (left, right) => {
418 if left == right {
419 Ok(())
420 } else {
421 Err(std::ops::ControlFlow::Continue(anyhow::anyhow!(
422 "assertion failed, left: {left:?} right: {right:?}"
423 )))
424 }
425 }
426 }
427 };
428}
429
430#[macro_export]
431macro_rules! poll_almost_equal {
432 ($left:expr_2021, $right:expr_2021) => {
433 match ($left, $right) {
434 (left, right) => $crate::util::almost_equal(left, right, 1_000_000)
435 .map_err(|e| std::ops::ControlFlow::Continue(anyhow::anyhow!(e))),
436 }
437 };
438}
439
440pub fn almost_equal(a: u64, b: u64, max: u64) -> Result<(), String> {
441 if a.abs_diff(b) <= max {
442 Ok(())
443 } else {
444 Err(format!(
445 "Expected difference is {max} but we found {}",
446 a.abs_diff(b)
447 ))
448 }
449}
450
451pub(crate) use cmd;
453
454pub async fn poll_with_timeout<Fut, R>(
461 name: &str,
462 timeout: Duration,
463 f: impl Fn() -> Fut,
464) -> Result<R>
465where
466 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
467{
468 const MIN_BACKOFF: Duration = Duration::from_millis(50);
469 const MAX_BACKOFF: Duration = Duration::from_secs(1);
470
471 let mut backoff = custom_backoff(MIN_BACKOFF, MAX_BACKOFF, None);
472 let start = now();
473 for attempt in 0u64.. {
474 let attempt_start = now();
475 match f().await {
476 Ok(value) => return Ok(value),
477 Err(ControlFlow::Break(err)) => {
478 return Err(err).with_context(|| format!("polling {name}"));
479 }
480 Err(ControlFlow::Continue(err))
481 if attempt_start
482 .duration_since(start)
483 .expect("time goes forward")
484 < timeout =>
485 {
486 debug!(target: LOG_DEVIMINT, %attempt, err = %err.fmt_compact_anyhow(), "Polling {name} failed, will retry...");
487 task::sleep(backoff.next().unwrap_or(MAX_BACKOFF)).await;
488 }
489 Err(ControlFlow::Continue(err)) => {
490 return Err(err).with_context(|| {
491 format!(
492 "Polling {name} failed after {attempt} retries (timeout: {}s)",
493 timeout.as_secs()
494 )
495 });
496 }
497 }
498 }
499
500 unreachable!();
501}
502
503const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_mins(1);
504const EXTRA_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(90);
505
506pub async fn poll<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
513where
514 Fut: Future<Output = Result<R, ControlFlow<anyhow::Error, anyhow::Error>>>,
515{
516 poll_with_timeout(
517 name,
518 if is_env_var_set("FM_EXTRA_LONG_POLL") {
519 EXTRA_LONG_POLL_TIMEOUT
520 } else {
521 DEFAULT_POLL_TIMEOUT
522 },
523 f,
524 )
525 .await
526}
527
528pub async fn poll_simple<Fut, R>(name: &str, f: impl Fn() -> Fut) -> Result<R>
529where
530 Fut: Future<Output = Result<R, anyhow::Error>>,
531{
532 poll(name, || async { f().await.map_err(ControlFlow::Continue) }).await
533}
534
535pub trait ToCmdExt {
537 fn cmd(self) -> Command;
538}
539
540impl ToCmdExt for &'_ str {
542 fn cmd(self) -> Command {
543 Command {
544 cmd: tokio::process::Command::new(self),
545 args_debug: vec![self.to_owned()],
546 }
547 }
548}
549
550impl ToCmdExt for Vec<String> {
551 fn cmd(self) -> Command {
552 to_command(self)
553 }
554}
555
556pub trait JsonValueExt {
557 fn to_typed<T: DeserializeOwned>(self) -> Result<T>;
558}
559
560impl JsonValueExt for serde_json::Value {
561 fn to_typed<T: DeserializeOwned>(self) -> Result<T> {
562 Ok(serde_json::from_value(self)?)
563 }
564}
565
566const GATEWAYD_FALLBACK: &str = "gatewayd";
567
568const FEDIMINTD_FALLBACK: &str = "fedimintd";
569
570const FEDIMINT_CLI_FALLBACK: &str = "fedimint-cli";
571
572pub fn get_fedimint_cli_path() -> Vec<String> {
573 get_command_str_for_alias(
574 &[FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV],
575 &[FEDIMINT_CLI_FALLBACK],
576 )
577}
578
579const GATEWAY_CLI_FALLBACK: &str = "gateway-cli";
580
581pub fn get_gateway_cli_path() -> Vec<String> {
582 get_command_str_for_alias(
583 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
584 &[GATEWAY_CLI_FALLBACK],
585 )
586}
587
588const LOAD_TEST_TOOL_FALLBACK: &str = "fedimint-load-test-tool";
589
590const LNCLI_FALLBACK: &str = "lncli";
591
592pub fn get_lncli_path() -> Vec<String> {
593 get_command_str_for_alias(&[FM_LNCLI_BASE_EXECUTABLE_ENV], &[LNCLI_FALLBACK])
594}
595
596const BITCOIN_CLI_FALLBACK: &str = "bitcoin-cli";
597
598pub fn get_bitcoin_cli_path() -> Vec<String> {
599 get_command_str_for_alias(
600 &[FM_BITCOIN_CLI_BASE_EXECUTABLE_ENV],
601 &[BITCOIN_CLI_FALLBACK],
602 )
603}
604
605const BITCOIND_FALLBACK: &str = "bitcoind";
606
607const LND_FALLBACK: &str = "lnd";
608
609const ESPLORA_FALLBACK: &str = "esplora";
610
611const RECOVERYTOOL_FALLBACK: &str = "fedimint-recoverytool";
612
613const DEVIMINT_FAUCET_FALLBACK: &str = "devimint";
614
615const FEDIMINT_DBTOOL_FALLBACK: &str = "fedimint-dbtool";
616
617pub fn get_fedimint_dbtool_cli_path() -> Vec<String> {
618 get_command_str_for_alias(
619 &[FM_FEDIMINT_DBTOOL_BASE_EXECUTABLE_ENV],
620 &[FEDIMINT_DBTOOL_FALLBACK],
621 )
622}
623
624fn version_hash_to_version(version_hash: &str) -> Result<Version> {
626 match version_hash {
627 "a8422b84102ab5fc768307215d5b20d807143f27" => Ok(Version::new(0, 2, 1)),
628 "a849377f6466b26bf9b2747242ff01fd4d4a031b" => Ok(Version::new(0, 2, 2)),
629 _ => Err(anyhow!("no version known for version hash: {version_hash}")),
630 }
631}
632
633pub struct FedimintdCmd;
634impl FedimintdCmd {
635 pub fn cmd(self) -> Command {
636 to_command(get_command_str_for_alias(
637 &[FM_FEDIMINTD_BASE_EXECUTABLE_ENV],
638 &[FEDIMINTD_FALLBACK],
639 ))
640 }
641
642 pub async fn version_or_default() -> Version {
644 match cmd!(FedimintdCmd, "--version").out_string().await {
645 Ok(version) => parse_clap_version(&version),
646 Err(_) => cmd!(FedimintdCmd, "version-hash")
647 .out_string()
648 .await
649 .map_or(DEFAULT_VERSION, |v| {
650 version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
651 }),
652 }
653 }
654}
655
656pub struct Gatewayd;
657impl Gatewayd {
658 pub fn cmd(self) -> Command {
659 to_command(get_command_str_for_alias(
660 &[FM_GATEWAYD_BASE_EXECUTABLE_ENV],
661 &[GATEWAYD_FALLBACK],
662 ))
663 }
664
665 pub async fn version_or_default() -> Version {
667 match cmd!(Gatewayd, "--version").out_string().await {
668 Ok(version) => parse_clap_version(&version),
669 Err(_) => cmd!(Gatewayd, "version-hash")
670 .out_string()
671 .await
672 .map_or(DEFAULT_VERSION, |v| {
673 version_hash_to_version(&v).unwrap_or(DEFAULT_VERSION)
674 }),
675 }
676 }
677}
678
679pub struct FedimintCli;
680impl FedimintCli {
681 pub fn cmd(self) -> Command {
682 to_command(get_command_str_for_alias(
683 &[FM_MINT_CLIENT_ENV],
684 &get_fedimint_cli_path()
685 .iter()
686 .map(String::as_str)
687 .collect::<Vec<_>>(),
688 ))
689 }
690
691 pub async fn version_or_default() -> Version {
693 match cmd!(FedimintCli, "--version").out_string().await {
694 Ok(version) => parse_clap_version(&version),
695 Err(_) => DEFAULT_VERSION,
696 }
697 }
698
699 pub async fn set_local_params_leader(
700 self,
701 peer: &PeerId,
702 auth: &ApiAuth,
703 endpoint: &str,
704 federation_size: Option<usize>,
705 ) -> Result<String> {
706 let mut command = cmd!(
707 self,
708 "--password",
709 auth.as_str(),
710 "admin",
711 "setup",
712 endpoint,
713 "set-local-params",
714 format!("Devimint Guardian {peer}"),
715 "--federation-name",
716 "Devimint Federation",
717 );
718
719 if let Some(size) = federation_size {
720 command = command.args(["--federation-size", &size.to_string()]);
721 }
722
723 let json = command.out_json().await?;
724
725 Ok(serde_json::from_value(json)?)
726 }
727
728 pub async fn set_local_params_follower(
729 self,
730 peer: &PeerId,
731 auth: &ApiAuth,
732 endpoint: &str,
733 ) -> Result<String> {
734 let json = cmd!(
735 self,
736 "--password",
737 auth.as_str(),
738 "admin",
739 "setup",
740 endpoint,
741 "set-local-params",
742 format!("Devimint Guardian {peer}")
743 )
744 .out_json()
745 .await?;
746
747 Ok(serde_json::from_value(json)?)
748 }
749
750 pub async fn add_peer(self, params: &str, auth: &ApiAuth, endpoint: &str) -> Result<()> {
751 cmd!(
752 self,
753 "--password",
754 auth.as_str(),
755 "admin",
756 "setup",
757 endpoint,
758 "add-peer",
759 params
760 )
761 .run()
762 .await
763 }
764
765 pub async fn setup_status(self, auth: &ApiAuth, endpoint: &str) -> Result<SetupStatus> {
766 let json = cmd!(
767 self,
768 "--password",
769 auth.as_str(),
770 "admin",
771 "setup",
772 endpoint,
773 "status",
774 )
775 .out_json()
776 .await?;
777
778 Ok(serde_json::from_value(json)?)
779 }
780
781 pub async fn start_dkg(self, auth: &ApiAuth, endpoint: &str) -> Result<()> {
782 cmd!(
783 self,
784 "--password",
785 auth.as_str(),
786 "admin",
787 "setup",
788 endpoint,
789 "start-dkg"
790 )
791 .run()
792 .await
793 }
794
795 pub async fn shutdown(self, auth: &ApiAuth, our_id: u64, session_count: u64) -> Result<()> {
796 cmd!(
797 self,
798 "--password",
799 auth.as_str(),
800 "--our-id",
801 our_id,
802 "admin",
803 "shutdown",
804 session_count,
805 )
806 .run()
807 .await
808 }
809
810 pub async fn status(self, auth: &ApiAuth, our_id: u64) -> Result<()> {
811 cmd!(
812 self,
813 "--password",
814 auth.as_str(),
815 "--our-id",
816 our_id,
817 "admin",
818 "status",
819 )
820 .run()
821 .await
822 }
823}
824
825pub struct LoadTestTool;
826impl LoadTestTool {
827 pub fn cmd(self) -> Command {
828 to_command(get_command_str_for_alias(
829 &[FM_LOAD_TEST_TOOL_BASE_EXECUTABLE_ENV],
830 &[LOAD_TEST_TOOL_FALLBACK],
831 ))
832 }
833}
834
835pub struct GatewayCli;
836impl GatewayCli {
837 pub fn cmd(self) -> Command {
838 to_command(get_command_str_for_alias(
839 &[FM_GATEWAY_CLI_BASE_EXECUTABLE_ENV],
840 &get_gateway_cli_path()
841 .iter()
842 .map(String::as_str)
843 .collect::<Vec<_>>(),
844 ))
845 }
846
847 pub async fn version_or_default() -> Version {
849 match cmd!(GatewayCli, "--version").out_string().await {
850 Ok(version) => parse_clap_version(&version),
851 Err(_) => DEFAULT_VERSION,
852 }
853 }
854}
855
856pub struct GatewayLndCli;
857impl GatewayLndCli {
858 pub fn cmd(self) -> Command {
859 to_command(get_command_str_for_alias(
860 &[FM_GWCLI_LND_ENV],
861 &["gateway-lnd"],
862 ))
863 }
864}
865
866pub struct GatewayLdkCli;
867impl GatewayLdkCli {
868 pub fn cmd(self) -> Command {
869 to_command(get_command_str_for_alias(
870 &[FM_GWCLI_LDK_ENV],
871 &["gateway-ldk"],
872 ))
873 }
874}
875
876pub struct LnCli;
877impl LnCli {
878 pub fn cmd(self) -> Command {
879 to_command(get_command_str_for_alias(
880 &[FM_LNCLI_ENV],
881 &get_lncli_path()
882 .iter()
883 .map(String::as_str)
884 .collect::<Vec<_>>(),
885 ))
886 }
887}
888
889pub struct BitcoinCli;
890impl BitcoinCli {
891 pub fn cmd(self) -> Command {
892 to_command(get_command_str_for_alias(
893 &[FM_BTC_CLIENT_ENV],
894 &get_bitcoin_cli_path()
895 .iter()
896 .map(String::as_str)
897 .collect::<Vec<_>>(),
898 ))
899 }
900}
901
902pub struct Bitcoind;
903impl Bitcoind {
904 pub fn cmd(self) -> Command {
905 to_command(get_command_str_for_alias(
906 &[FM_BITCOIND_BASE_EXECUTABLE_ENV],
907 &[BITCOIND_FALLBACK],
908 ))
909 }
910}
911
912pub struct Lnd;
913impl Lnd {
914 pub fn cmd(self) -> Command {
915 to_command(get_command_str_for_alias(
916 &[FM_LND_BASE_EXECUTABLE_ENV],
917 &[LND_FALLBACK],
918 ))
919 }
920}
921
922pub struct Esplora;
923impl Esplora {
924 pub fn cmd(self) -> Command {
925 to_command(get_command_str_for_alias(
926 &[FM_ESPLORA_BASE_EXECUTABLE_ENV],
927 &[ESPLORA_FALLBACK],
928 ))
929 }
930}
931
932pub struct Recoverytool;
933impl Recoverytool {
934 pub fn cmd(self) -> Command {
935 to_command(get_command_str_for_alias(
936 &[FM_RECOVERYTOOL_BASE_EXECUTABLE_ENV],
937 &[RECOVERYTOOL_FALLBACK],
938 ))
939 }
940}
941
942pub struct DevimintFaucet;
943impl DevimintFaucet {
944 pub fn cmd(self) -> Command {
945 to_command(get_command_str_for_alias(
946 &[FM_DEVIMINT_FAUCET_BASE_EXECUTABLE_ENV],
947 &[DEVIMINT_FAUCET_FALLBACK],
948 ))
949 }
950}
951
952pub struct Recurringd;
953impl Recurringd {
954 pub fn cmd(self) -> Command {
955 to_command(get_command_str_for_alias(
956 &[FM_RECURRINGD_BASE_EXECUTABLE_ENV],
957 &["fedimint-recurringd"],
958 ))
959 }
960}
961
962fn get_command_str_for_alias(aliases: &[&str], default: &[&str]) -> Vec<String> {
963 for alias in aliases {
965 if let Ok(cmd) = std::env::var(alias) {
966 return cmd.split_whitespace().map(ToOwned::to_owned).collect();
967 }
968 }
969 default.iter().map(ToString::to_string).collect()
971}
972
973fn to_command(cli: Vec<String>) -> Command {
974 let mut cmd = tokio::process::Command::new(&cli[0]);
975 cmd.args(&cli[1..]);
976 Command {
977 cmd,
978 args_debug: cli,
979 }
980}
981
982pub fn supports_lnv2() -> bool {
983 std::env::var_os(FM_ENABLE_MODULE_LNV2_ENV).is_none()
984 || is_env_var_set(FM_ENABLE_MODULE_LNV2_ENV)
985}
986
987pub fn supports_wallet_v2() -> bool {
988 is_env_var_set(FM_ENABLE_MODULE_WALLETV2_ENV)
989}
990
991pub fn supports_mint_v2() -> bool {
992 is_env_var_set(FM_ENABLE_MODULE_MINTV2_ENV)
993}
994
995pub fn is_backwards_compatibility_test() -> bool {
997 is_env_var_set(FM_BACKWARDS_COMPATIBILITY_TEST_ENV)
998}
999
1000pub async fn use_matching_fedimint_cli_for_dkg() -> Result<(String, String)> {
1004 let pkg_version = semver::Version::parse(env!("CARGO_PKG_VERSION"))?;
1005 let fedimintd_version = crate::util::FedimintdCmd::version_or_default().await;
1006 let original_fedimint_cli_path = crate::util::get_fedimint_cli_path().join(" ");
1007
1008 if pkg_version == fedimintd_version {
1009 unsafe { std::env::remove_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV) };
1014 } else {
1015 let parsed_fedimintd_version = fedimintd_version.to_string().replace(['-', '.'], "_");
1016
1017 let fedimint_cli_path_var = format!("fm_bin_fedimint_cli_v{parsed_fedimintd_version}");
1019 let fedimint_cli_path = std::env::var(fedimint_cli_path_var)?;
1020 unsafe { std::env::set_var(FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV, fedimint_cli_path) };
1022 }
1023
1024 let original_fm_mint_client = std::env::var(FM_MINT_CLIENT_ENV)?;
1025 let fm_client_dir = std::env::var(FM_CLIENT_DIR_ENV)?;
1026 let fm_client_dir_path_buf: PathBuf = PathBuf::from(fm_client_dir);
1027
1028 let fm_mint_client: String = format!(
1029 "{fedimint_cli} --data-dir {datadir}",
1030 fedimint_cli = crate::util::get_fedimint_cli_path().join(" "),
1031 datadir = crate::vars::utf8(&fm_client_dir_path_buf)
1032 );
1033 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, fm_mint_client) };
1035
1036 Ok((original_fedimint_cli_path, original_fm_mint_client))
1037}
1038
1039pub fn use_fedimint_cli(original_fedimint_cli_path: String, original_fm_mint_client: String) {
1041 unsafe {
1043 std::env::set_var(
1044 FM_FEDIMINT_CLI_BASE_EXECUTABLE_ENV,
1045 original_fedimint_cli_path,
1046 );
1047 };
1048
1049 unsafe { std::env::set_var(FM_MINT_CLIENT_ENV, original_fm_mint_client) };
1051}
1052
1053fn parse_clap_version(res: &str) -> Version {
1056 match res.split(' ').collect::<Vec<&str>>().as_slice() {
1057 [_binary, version] => Version::parse(version).unwrap_or(DEFAULT_VERSION),
1058 _ => DEFAULT_VERSION,
1059 }
1060}
1061
1062#[test]
1063fn test_parse_clap_version() -> Result<()> {
1064 let version_str = "fedimintd 0.3.0-alpha";
1065 let expected_version = Version::parse("0.3.0-alpha")?;
1066 assert_eq!(expected_version, parse_clap_version(version_str));
1067
1068 let version_str = "fedimintd 0.3.12";
1069 let expected_version = Version::parse("0.3.12")?;
1070 assert_eq!(expected_version, parse_clap_version(version_str));
1071
1072 let version_str = "fedimint-cli 2.12.2-rc22";
1073 let expected_version = Version::parse("2.12.2-rc22")?;
1074 assert_eq!(expected_version, parse_clap_version(version_str));
1075
1076 let version_str = "bad version";
1077 let expected_version = DEFAULT_VERSION;
1078 assert_eq!(expected_version, parse_clap_version(version_str));
1079
1080 Ok(())
1081}