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