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