1#![deny(clippy::pedantic)]
2#![allow(clippy::doc_markdown)]
3#![allow(clippy::missing_errors_doc)]
4#![allow(clippy::missing_panics_doc)]
5#![allow(clippy::module_name_repetitions)]
6#![allow(clippy::must_use_candidate)]
7#![allow(clippy::ref_option)]
8#![allow(clippy::return_self_not_must_use)]
9#![allow(clippy::too_many_lines)]
10#![allow(clippy::large_futures)]
11
12mod client;
13mod db_locked;
14pub mod envs;
15mod utils;
16
17use core::fmt;
18use std::collections::BTreeMap;
19use std::fmt::Debug;
20use std::io::{Read, Write};
21use std::path::{Path, PathBuf};
22use std::process::exit;
23use std::str::FromStr;
24use std::sync::Arc;
25use std::time::Duration;
26use std::{fs, result};
27
28use anyhow::{Context, format_err};
29use clap::{Args, CommandFactory, Parser, Subcommand};
30use client::ModuleSelector;
31use db_locked::LockedBuilder;
32#[cfg(feature = "tor")]
33use envs::FM_USE_TOR_ENV;
34use envs::{FM_API_SECRET_ENV, SALT_FILE};
35use fedimint_aead::{encrypted_read, encrypted_write, get_encryption_key};
36use fedimint_api_client::api::net::Connector;
37use fedimint_api_client::api::{DynGlobalApi, FederationApiExt, FederationError};
38use fedimint_bip39::{Bip39RootSecretStrategy, Mnemonic};
39use fedimint_client::module::meta::{FetchKind, LegacyMetaSource, MetaSource};
40use fedimint_client::module::module::init::ClientModuleInit;
41use fedimint_client::module_init::ClientModuleInitRegistry;
42use fedimint_client::secret::{RootSecretStrategy, get_default_client_secret};
43use fedimint_client::{AdminCreds, Client, ClientBuilder, ClientHandleArc};
44use fedimint_core::config::{FederationId, FederationIdPrefix};
45use fedimint_core::core::{ModuleInstanceId, OperationId};
46use fedimint_core::db::{Database, DatabaseValue};
47use fedimint_core::invite_code::InviteCode;
48use fedimint_core::module::{ApiAuth, ApiRequestErased};
49use fedimint_core::util::{SafeUrl, backoff_util, handle_version_hash_command, retry};
50use fedimint_core::{Amount, PeerId, TieredMulti, fedimint_build_code_version_env, runtime};
51use fedimint_eventlog::EventLogId;
52use fedimint_ln_client::LightningClientInit;
53use fedimint_logging::{LOG_CLIENT, TracingSetup};
54use fedimint_meta_client::{MetaClientInit, MetaModuleMetaSourceWithFallback};
55use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, SpendableNote};
56use fedimint_wallet_client::api::WalletFederationApi;
57use fedimint_wallet_client::{WalletClientInit, WalletClientModule};
58use futures::future::pending;
59use itertools::Itertools;
60use rand::thread_rng;
61use serde::{Deserialize, Serialize};
62use serde_json::{Value, json};
63use thiserror::Error;
64use tracing::{debug, info, warn};
65use utils::parse_peer_id;
66
67use crate::client::ClientCmd;
68use crate::envs::{FM_CLIENT_DIR_ENV, FM_OUR_ID_ENV, FM_PASSWORD_ENV};
69
70#[derive(Serialize)]
72#[serde(rename_all = "snake_case")]
73#[serde(untagged)]
74enum CliOutput {
75 VersionHash {
76 hash: String,
77 },
78
79 UntypedApiOutput {
80 value: Value,
81 },
82
83 WaitBlockCount {
84 reached: u64,
85 },
86
87 InviteCode {
88 invite_code: InviteCode,
89 },
90
91 DecodeInviteCode {
92 url: SafeUrl,
93 federation_id: FederationId,
94 },
95
96 JoinFederation {
97 joined: String,
98 },
99
100 DecodeTransaction {
101 transaction: String,
102 },
103
104 EpochCount {
105 count: u64,
106 },
107
108 ConfigDecrypt,
109
110 ConfigEncrypt,
111
112 Raw(serde_json::Value),
113}
114
115impl fmt::Display for CliOutput {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
118 }
119}
120
121type CliResult<E> = Result<E, CliError>;
123
124type CliOutputResult = Result<CliOutput, CliError>;
126
127#[derive(Serialize, Error)]
129#[serde(tag = "error", rename_all(serialize = "snake_case"))]
130struct CliError {
131 error: String,
132}
133
134trait CliResultExt<O, E> {
137 fn map_err_cli(self) -> Result<O, CliError>;
139 fn map_err_cli_msg(self, msg: impl fmt::Display + Send + Sync + 'static)
141 -> Result<O, CliError>;
142}
143
144impl<O, E> CliResultExt<O, E> for result::Result<O, E>
145where
146 E: Into<anyhow::Error>,
147{
148 fn map_err_cli(self) -> Result<O, CliError> {
149 self.map_err(|e| {
150 let e = e.into();
151 CliError {
152 error: format!("{e:#}"),
153 }
154 })
155 }
156
157 fn map_err_cli_msg(
158 self,
159 msg: impl fmt::Display + Send + Sync + 'static,
160 ) -> Result<O, CliError> {
161 self.map_err(|e| Into::<anyhow::Error>::into(e))
162 .context(msg)
163 .map_err(|e| CliError {
164 error: format!("{e:#}"),
165 })
166 }
167}
168
169trait CliOptionExt<O> {
172 fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError>;
173}
174
175impl<O> CliOptionExt<O> for Option<O> {
176 fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError> {
177 self.ok_or_else(|| CliError { error: msg.into() })
178 }
179}
180
181impl From<FederationError> for CliError {
183 fn from(e: FederationError) -> Self {
184 CliError {
185 error: e.to_string(),
186 }
187 }
188}
189
190impl Debug for CliError {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 f.debug_struct("CliError")
193 .field("error", &self.error)
194 .finish()
195 }
196}
197
198impl fmt::Display for CliError {
199 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
200 let json = serde_json::to_value(self).expect("CliError is valid json");
201 let json_as_string =
202 serde_json::to_string_pretty(&json).expect("valid json is serializable");
203 write!(f, "{json_as_string}")
204 }
205}
206
207#[derive(Parser, Clone)]
208#[command(version)]
209struct Opts {
210 #[arg(long = "data-dir", env = FM_CLIENT_DIR_ENV)]
212 data_dir: Option<PathBuf>,
213
214 #[arg(env = FM_OUR_ID_ENV, long, value_parser = parse_peer_id)]
216 our_id: Option<PeerId>,
217
218 #[arg(long, env = FM_PASSWORD_ENV)]
220 password: Option<String>,
221
222 #[cfg(feature = "tor")]
223 #[arg(long, env = FM_USE_TOR_ENV)]
225 use_tor: bool,
226
227 #[arg(short = 'v', long)]
230 verbose: bool,
231
232 #[clap(subcommand)]
233 command: Command,
234}
235
236impl Opts {
237 fn data_dir(&self) -> CliResult<&PathBuf> {
238 self.data_dir
239 .as_ref()
240 .ok_or_cli_msg("`--data-dir=` argument not set.")
241 }
242
243 async fn data_dir_create(&self) -> CliResult<&PathBuf> {
245 let dir = self.data_dir()?;
246
247 tokio::fs::create_dir_all(&dir).await.map_err_cli()?;
248
249 Ok(dir)
250 }
251
252 async fn admin_client(
253 &self,
254 peer_urls: &BTreeMap<PeerId, SafeUrl>,
255 api_secret: &Option<String>,
256 ) -> CliResult<DynGlobalApi> {
257 let our_id = self.our_id.ok_or_cli_msg("Admin client needs our-id set")?;
258
259 DynGlobalApi::new_admin(
260 our_id,
261 peer_urls
262 .get(&our_id)
263 .cloned()
264 .context("Our peer URL not found in config")
265 .map_err_cli()?,
266 api_secret,
267 )
268 .await
269 .map_err(|e| CliError {
270 error: e.to_string(),
271 })
272 }
273
274 fn auth(&self) -> CliResult<ApiAuth> {
275 let password = self
276 .password
277 .clone()
278 .ok_or_cli_msg("CLI needs password set")?;
279 Ok(ApiAuth(password))
280 }
281
282 async fn load_rocks_db(&self) -> CliResult<Database> {
283 debug!(target: LOG_CLIENT, "Loading client database");
284 let db_path = self.data_dir_create().await?.join("client.db");
285 let lock_path = db_path.with_extension("db.lock");
286 Ok(LockedBuilder::new(&lock_path)
287 .map_err_cli_msg("could not lock database")?
288 .with_db(
289 fedimint_rocksdb::RocksDb::open(db_path)
290 .map_err_cli_msg("could not open database")?,
291 )
292 .into())
293 }
294
295 #[allow(clippy::unused_self)]
296 fn connector(&self) -> Connector {
297 #[cfg(feature = "tor")]
298 if self.use_tor {
299 Connector::tor()
300 } else {
301 Connector::default()
302 }
303 #[cfg(not(feature = "tor"))]
304 Connector::default()
305 }
306}
307
308async fn load_or_generate_mnemonic(db: &Database) -> Result<Mnemonic, CliError> {
309 Ok(
310 if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(db).await {
311 Mnemonic::from_entropy(&entropy).map_err_cli()?
312 } else {
313 debug!(
314 target: LOG_CLIENT,
315 "Generating mnemonic and writing entropy to client storage"
316 );
317 let mnemonic = Bip39RootSecretStrategy::<12>::random(&mut thread_rng());
318 Client::store_encodable_client_secret(db, mnemonic.to_entropy())
319 .await
320 .map_err_cli()?;
321 mnemonic
322 },
323 )
324}
325
326#[derive(Subcommand, Clone)]
327enum Command {
328 VersionHash,
330
331 #[clap(flatten)]
332 Client(client::ClientCmd),
333
334 #[clap(subcommand)]
335 Admin(AdminCmd),
336
337 #[clap(subcommand)]
338 Dev(DevCmd),
339
340 InviteCode {
342 peer: PeerId,
343 },
344
345 JoinFederation {
347 invite_code: String,
348 },
349
350 Completion {
351 shell: clap_complete::Shell,
352 },
353}
354
355#[allow(clippy::large_enum_variant)]
356#[derive(Debug, Clone, Subcommand)]
357enum AdminCmd {
358 Status,
360
361 Audit,
363
364 GuardianConfigBackup,
366
367 ConfigGen(ConfigGenAdminArgs),
368 SignApiAnnouncement {
371 api_url: SafeUrl,
373 #[clap(long)]
376 override_url: Option<SafeUrl>,
377 },
378 Shutdown {
380 session_idx: u64,
382 },
383 BackupStatistics,
385}
386
387#[derive(Debug, Clone, Args)]
388struct ConfigGenAdminArgs {
389 #[arg(long, env = "FM_WS_URL")]
390 ws: SafeUrl,
391
392 #[arg(env = FM_API_SECRET_ENV)]
393 api_secret: Option<String>,
394
395 #[clap(subcommand)]
396 subcommand: ConfigGenAdminCmd,
397}
398
399#[derive(Debug, Clone, Subcommand)]
400enum ConfigGenAdminCmd {
401 ServerStatus,
402 SetLocalParams {
403 name: String,
404 #[clap(long)]
405 federation_name: Option<String>,
406 },
407 AddPeerConnectionInfo {
408 info: String,
409 },
410 StartDkg,
411}
412
413#[derive(Debug, Clone, Subcommand)]
414enum DecodeType {
415 InviteCode { invite_code: InviteCode },
417 Notes { notes: OOBNotes },
419 Transaction { hex_string: String },
421}
422
423#[derive(Debug, Clone, Deserialize, Serialize)]
424struct OOBNotesJson {
425 federation_id_prefix: String,
426 notes: TieredMulti<SpendableNote>,
427}
428
429#[derive(Debug, Clone, Subcommand)]
430enum EncodeType {
431 InviteCode {
433 #[clap(long)]
434 url: SafeUrl,
435 #[clap(long = "federation_id")]
436 federation_id: FederationId,
437 #[clap(long = "peer")]
438 peer: PeerId,
439 #[arg(env = FM_API_SECRET_ENV)]
440 api_secret: Option<String>,
441 },
442
443 Notes { notes_json: String },
445}
446
447#[derive(Debug, Clone, Subcommand)]
448enum DevCmd {
449 #[command(after_long_help = r#"
453Examples:
454
455 fedimint-cli dev api --peer-id 0 config '"fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj"'
456 "#)]
457 Api {
458 method: String,
460 #[clap(default_value = "null")]
465 params: String,
466 #[clap(long = "peer-id")]
468 peer_id: Option<u16>,
469
470 #[clap(long = "module")]
472 module: Option<ModuleSelector>,
473
474 #[clap(long, requires = "peer_id")]
477 password: Option<String>,
478 },
479
480 ApiAnnouncements,
481
482 AdvanceNoteIdx {
484 #[clap(long, default_value = "1")]
485 count: usize,
486
487 #[clap(long)]
488 amount: Amount,
489 },
490
491 WaitBlockCount {
493 count: u64,
494 },
495
496 Wait {
498 seconds: Option<f32>,
500 },
501
502 WaitComplete,
504
505 Decode {
507 #[clap(subcommand)]
508 decode_type: DecodeType,
509 },
510
511 Encode {
513 #[clap(subcommand)]
514 encode_type: EncodeType,
515 },
516
517 SessionCount,
519
520 ConfigDecrypt {
521 #[arg(long = "in-file")]
523 in_file: PathBuf,
524 #[arg(long = "out-file")]
526 out_file: PathBuf,
527 #[arg(long = "salt-file")]
530 salt_file: Option<PathBuf>,
531 #[arg(env = FM_PASSWORD_ENV)]
533 password: String,
534 },
535
536 ConfigEncrypt {
537 #[arg(long = "in-file")]
539 in_file: PathBuf,
540 #[arg(long = "out-file")]
542 out_file: PathBuf,
543 #[arg(long = "salt-file")]
546 salt_file: Option<PathBuf>,
547 #[arg(env = FM_PASSWORD_ENV)]
549 password: String,
550 },
551
552 ListOperationStates {
555 operation_id: OperationId,
556 },
557 MetaFields,
561 PeerVersion {
563 #[clap(long)]
564 peer_id: u16,
565 },
566 ShowEventLog {
568 #[arg(long)]
569 pos: Option<EventLogId>,
570 #[arg(long, default_value = "10")]
571 limit: u64,
572 },
573}
574
575#[derive(Debug, Serialize, Deserialize)]
576#[serde(rename_all = "snake_case")]
577struct PayRequest {
578 notes: TieredMulti<SpendableNote>,
579 invoice: lightning_invoice::Bolt11Invoice,
580}
581
582pub struct FedimintCli {
583 module_inits: ClientModuleInitRegistry,
584 cli_args: Opts,
585}
586
587impl FedimintCli {
588 pub fn new(version_hash: &str) -> anyhow::Result<FedimintCli> {
590 assert_eq!(
591 fedimint_build_code_version_env!().len(),
592 version_hash.len(),
593 "version_hash must have an expected length"
594 );
595
596 handle_version_hash_command(version_hash);
597
598 let cli_args = Opts::parse();
599 let base_level = if cli_args.verbose { "debug" } else { "info" };
600 TracingSetup::default()
601 .with_base_level(base_level)
602 .init()
603 .expect("tracing initializes");
604
605 let version = env!("CARGO_PKG_VERSION");
606 debug!(target: LOG_CLIENT, "Starting fedimint-cli (version: {version} version_hash: {version_hash})");
607
608 Ok(Self {
609 module_inits: ClientModuleInitRegistry::new(),
610 cli_args,
611 })
612 }
613
614 pub fn with_module<T>(mut self, r#gen: T) -> Self
615 where
616 T: ClientModuleInit + 'static + Send + Sync,
617 {
618 self.module_inits.attach(r#gen);
619 self
620 }
621
622 pub fn with_default_modules(self) -> Self {
623 self.with_module(LightningClientInit::default())
624 .with_module(MintClientInit)
625 .with_module(WalletClientInit::default())
626 .with_module(MetaClientInit)
627 .with_module(fedimint_lnv2_client::LightningClientInit::default())
628 }
629
630 pub async fn run(&mut self) {
631 match self.handle_command(self.cli_args.clone()).await {
632 Ok(output) => {
633 let _ = writeln!(std::io::stdout(), "{output}");
635 }
636 Err(err) => {
637 debug!(target: LOG_CLIENT, err = %err.error.as_str(), "Command failed");
638 let _ = writeln!(std::io::stdout(), "{err}");
639 exit(1);
640 }
641 }
642 }
643
644 async fn make_client_builder(&self, cli: &Opts) -> CliResult<ClientBuilder> {
645 let db = cli.load_rocks_db().await?;
646 let mut client_builder = Client::builder(db).await.map_err_cli()?;
647 client_builder.with_module_inits(self.module_inits.clone());
648 client_builder.with_primary_module_kind(fedimint_mint_client::KIND);
649
650 #[cfg(feature = "tor")]
651 if cli.use_tor {
652 client_builder.with_tor_connector();
653 }
654
655 Ok(client_builder)
656 }
657
658 async fn client_join(
659 &mut self,
660 cli: &Opts,
661 invite_code: InviteCode,
662 ) -> CliResult<ClientHandleArc> {
663 let client_config = cli
664 .connector()
665 .download_from_invite_code(&invite_code)
666 .await
667 .map_err_cli()?;
668
669 let client_builder = self.make_client_builder(cli).await?;
670
671 let mnemonic = load_or_generate_mnemonic(client_builder.db_no_decoders()).await?;
672
673 let client = client_builder
674 .join(
675 get_default_client_secret(
676 &Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
677 &client_config.global.calculate_federation_id(),
678 ),
679 client_config.clone(),
680 invite_code.api_secret(),
681 )
682 .await
683 .map(Arc::new)
684 .map_err_cli()?;
685
686 print_welcome_message(&client).await;
687 log_expiration_notice(&client).await;
688
689 Ok(client)
690 }
691
692 async fn client_open(&self, cli: &Opts) -> CliResult<ClientHandleArc> {
693 let mut client_builder = self.make_client_builder(cli).await?;
694
695 if let Some(our_id) = cli.our_id {
696 client_builder.set_admin_creds(AdminCreds {
697 peer_id: our_id,
698 auth: cli.auth()?,
699 });
700 }
701
702 let mnemonic = Mnemonic::from_entropy(
703 &Client::load_decodable_client_secret::<Vec<u8>>(client_builder.db_no_decoders())
704 .await
705 .map_err_cli()?,
706 )
707 .map_err_cli()?;
708
709 let config = client_builder.load_existing_config().await.map_err_cli()?;
710
711 let federation_id = config.calculate_federation_id();
712
713 let client = client_builder
714 .open(get_default_client_secret(
715 &Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
716 &federation_id,
717 ))
718 .await
719 .map(Arc::new)
720 .map_err_cli()?;
721
722 log_expiration_notice(&client).await;
723
724 Ok(client)
725 }
726
727 async fn client_recover(
728 &mut self,
729 cli: &Opts,
730 mnemonic: Mnemonic,
731 invite_code: InviteCode,
732 ) -> CliResult<ClientHandleArc> {
733 let builder = self.make_client_builder(cli).await?;
734
735 let client_config = cli
736 .connector()
737 .download_from_invite_code(&invite_code)
738 .await
739 .map_err_cli()?;
740
741 match Client::load_decodable_client_secret_opt::<Vec<u8>>(builder.db_no_decoders())
742 .await
743 .map_err_cli()?
744 {
745 Some(existing) => {
746 if existing != mnemonic.to_entropy() {
747 Err(anyhow::anyhow!("Previously set mnemonic does not match")).map_err_cli()?;
748 }
749 }
750 None => {
751 Client::store_encodable_client_secret(
752 builder.db_no_decoders(),
753 mnemonic.to_entropy(),
754 )
755 .await
756 .map_err_cli()?;
757 }
758 }
759
760 let root_secret = get_default_client_secret(
761 &Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
762 &client_config.calculate_federation_id(),
763 );
764 let backup = builder
765 .download_backup_from_federation(&root_secret, &client_config, invite_code.api_secret())
766 .await
767 .map_err_cli()?;
768 let client = builder
769 .recover(
770 root_secret,
771 client_config.clone(),
772 invite_code.api_secret(),
773 backup,
774 )
775 .await
776 .map(Arc::new)
777 .map_err_cli()?;
778
779 print_welcome_message(&client).await;
780 log_expiration_notice(&client).await;
781
782 Ok(client)
783 }
784
785 async fn handle_command(&mut self, cli: Opts) -> CliOutputResult {
786 match cli.command.clone() {
787 Command::InviteCode { peer } => {
788 let client = self.client_open(&cli).await?;
789
790 let invite_code = client
791 .invite_code(peer)
792 .await
793 .ok_or_cli_msg("peer not found")?;
794
795 Ok(CliOutput::InviteCode { invite_code })
796 }
797 Command::JoinFederation { invite_code } => {
798 {
799 let invite_code: InviteCode = InviteCode::from_str(&invite_code)
800 .map_err_cli_msg("invalid invite code")?;
801
802 let _client = self.client_join(&cli, invite_code).await?;
804 }
805
806 Ok(CliOutput::JoinFederation {
807 joined: invite_code,
808 })
809 }
810 Command::VersionHash => Ok(CliOutput::VersionHash {
811 hash: fedimint_build_code_version_env!().to_string(),
812 }),
813 Command::Client(ClientCmd::Restore {
814 mnemonic,
815 invite_code,
816 }) => {
817 let invite_code: InviteCode =
818 InviteCode::from_str(&invite_code).map_err_cli_msg("invalid invite code")?;
819 let mnemonic = Mnemonic::from_str(&mnemonic).map_err_cli()?;
820 let client = self.client_recover(&cli, mnemonic, invite_code).await?;
821
822 debug!(target: LOG_CLIENT, "Waiting for mint module recovery to finish");
825 client.wait_for_all_recoveries().await.map_err_cli()?;
826
827 debug!(target: LOG_CLIENT, "Recovery complete");
828
829 Ok(CliOutput::Raw(serde_json::to_value(()).unwrap()))
830 }
831 Command::Client(command) => {
832 let client = self.client_open(&cli).await?;
833 Ok(CliOutput::Raw(
834 client::handle_command(command, client)
835 .await
836 .map_err_cli()?,
837 ))
838 }
839 Command::Admin(AdminCmd::Audit) => {
840 let client = self.client_open(&cli).await?;
841
842 let audit = cli
843 .admin_client(&client.get_peer_urls().await, client.api_secret())
844 .await?
845 .audit(cli.auth()?)
846 .await?;
847 Ok(CliOutput::Raw(
848 serde_json::to_value(audit).map_err_cli_msg("invalid response")?,
849 ))
850 }
851 Command::Admin(AdminCmd::Status) => {
852 let client = self.client_open(&cli).await?;
853
854 let status = cli
855 .admin_client(&client.get_peer_urls().await, client.api_secret())
856 .await?
857 .status()
858 .await?;
859 Ok(CliOutput::Raw(
860 serde_json::to_value(status).map_err_cli_msg("invalid response")?,
861 ))
862 }
863 Command::Admin(AdminCmd::GuardianConfigBackup) => {
864 let client = self.client_open(&cli).await?;
865
866 let guardian_config_backup = cli
867 .admin_client(&client.get_peer_urls().await, client.api_secret())
868 .await?
869 .guardian_config_backup(cli.auth()?)
870 .await?;
871 Ok(CliOutput::Raw(
872 serde_json::to_value(guardian_config_backup)
873 .map_err_cli_msg("invalid response")?,
874 ))
875 }
876 Command::Admin(AdminCmd::ConfigGen(dkg_args)) => self
877 .handle_admin_config_gen_command(cli, dkg_args)
878 .await
879 .map(CliOutput::Raw)
880 .map_err_cli_msg("Config Gen Error"),
881 Command::Admin(AdminCmd::SignApiAnnouncement {
882 api_url,
883 override_url,
884 }) => {
885 let client = self.client_open(&cli).await?;
886
887 if !["ws", "wss"].contains(&api_url.scheme()) {
888 return Err(CliError {
889 error: format!(
890 "Unsupported URL scheme {}, use ws:// or wss://",
891 api_url.scheme()
892 ),
893 });
894 }
895
896 let announcement = cli
897 .admin_client(
898 &override_url
899 .and_then(|url| Some(vec![(cli.our_id?, url)].into_iter().collect()))
900 .unwrap_or(client.get_peer_urls().await),
901 client.api_secret(),
902 )
903 .await?
904 .sign_api_announcement(api_url, cli.auth()?)
905 .await?;
906
907 Ok(CliOutput::Raw(
908 serde_json::to_value(announcement).map_err_cli_msg("invalid response")?,
909 ))
910 }
911 Command::Admin(AdminCmd::Shutdown { session_idx }) => {
912 let client = self.client_open(&cli).await?;
913
914 cli.admin_client(&client.get_peer_urls().await, client.api_secret())
915 .await?
916 .shutdown(Some(session_idx), cli.auth()?)
917 .await?;
918
919 Ok(CliOutput::Raw(json!(null)))
920 }
921 Command::Admin(AdminCmd::BackupStatistics) => {
922 let client = self.client_open(&cli).await?;
923
924 let backup_statistics = cli
925 .admin_client(&client.get_peer_urls().await, client.api_secret())
926 .await?
927 .backup_statistics(cli.auth()?)
928 .await?;
929
930 Ok(CliOutput::Raw(
931 serde_json::to_value(backup_statistics).expect("Can be encoded"),
932 ))
933 }
934 Command::Dev(DevCmd::Api {
935 method,
936 params,
937 peer_id,
938 password: auth,
939 module,
940 }) => {
941 let params = serde_json::from_str::<Value>(¶ms).unwrap_or_else(|err| {
944 debug!(
945 target: LOG_CLIENT,
946 "Failed to serialize params:{}. Converting it to JSON string",
947 err
948 );
949
950 serde_json::Value::String(params)
951 });
952
953 let mut params = ApiRequestErased::new(params);
954 if let Some(auth) = auth {
955 params = params.with_auth(ApiAuth(auth));
956 }
957 let client = self.client_open(&cli).await?;
958
959 let api = client.api_clone();
960
961 let module_api = match module {
962 Some(selector) => {
963 Some(api.with_module(selector.resolve(&client).map_err_cli()?))
964 }
965 None => None,
966 };
967
968 let response: Value = match (peer_id, module_api) {
969 (Some(peer_id), Some(module_api)) => module_api
970 .request_raw(peer_id.into(), &method, ¶ms)
971 .await
972 .map_err_cli()?,
973 (Some(peer_id), None) => api
974 .request_raw(peer_id.into(), &method, ¶ms)
975 .await
976 .map_err_cli()?,
977 (None, Some(module_api)) => module_api
978 .request_current_consensus(method, params)
979 .await
980 .map_err_cli()?,
981 (None, None) => api
982 .request_current_consensus(method, params)
983 .await
984 .map_err_cli()?,
985 };
986
987 Ok(CliOutput::UntypedApiOutput { value: response })
988 }
989 Command::Dev(DevCmd::AdvanceNoteIdx { count, amount }) => {
990 let client = self.client_open(&cli).await?;
991
992 let mint = client
993 .get_first_module::<MintClientModule>()
994 .map_err_cli_msg("can't get mint module")?;
995
996 for _ in 0..count {
997 mint.advance_note_idx(amount)
998 .await
999 .map_err_cli_msg("failed to advance the note_idx")?;
1000 }
1001
1002 Ok(CliOutput::Raw(serde_json::Value::Null))
1003 }
1004 Command::Dev(DevCmd::ApiAnnouncements) => {
1005 let client = self.client_open(&cli).await?;
1006 let announcements = client.get_peer_url_announcements().await;
1007 Ok(CliOutput::Raw(
1008 serde_json::to_value(announcements).expect("Can be encoded"),
1009 ))
1010 }
1011 Command::Dev(DevCmd::WaitBlockCount { count: target }) => retry(
1012 "wait_block_count",
1013 backoff_util::custom_backoff(
1014 Duration::from_millis(100),
1015 Duration::from_secs(5),
1016 None,
1017 ),
1018 || async {
1019 let client = self.client_open(&cli).await?;
1020 let wallet = client.get_first_module::<WalletClientModule>()?;
1021 let count = client
1022 .api()
1023 .with_module(wallet.id)
1024 .fetch_consensus_block_count()
1025 .await?;
1026 if count >= target {
1027 Ok(CliOutput::WaitBlockCount { reached: count })
1028 } else {
1029 info!(target: LOG_CLIENT, current=count, target, "Block count not reached");
1030 Err(format_err!("target not reached"))
1031 }
1032 },
1033 )
1034 .await
1035 .map_err_cli(),
1036
1037 Command::Dev(DevCmd::WaitComplete) => {
1038 let client = self.client_open(&cli).await?;
1039 client
1040 .wait_for_all_active_state_machines()
1041 .await
1042 .map_err_cli_msg("failed to wait for all active state machines")?;
1043 Ok(CliOutput::Raw(serde_json::Value::Null))
1044 }
1045 Command::Dev(DevCmd::Wait { seconds }) => {
1046 let _client = self.client_open(&cli).await?;
1047 if let Some(secs) = seconds {
1048 runtime::sleep(Duration::from_secs_f32(secs)).await;
1049 } else {
1050 pending::<()>().await;
1051 }
1052 Ok(CliOutput::Raw(serde_json::Value::Null))
1053 }
1054 Command::Dev(DevCmd::Decode { decode_type }) => match decode_type {
1055 DecodeType::InviteCode { invite_code } => Ok(CliOutput::DecodeInviteCode {
1056 url: invite_code.url(),
1057 federation_id: invite_code.federation_id(),
1058 }),
1059 DecodeType::Notes { notes } => {
1060 let notes_json = notes
1061 .notes_json()
1062 .map_err_cli_msg("failed to decode notes")?;
1063 Ok(CliOutput::Raw(notes_json))
1064 }
1065 DecodeType::Transaction { hex_string } => {
1066 let bytes: Vec<u8> = hex::FromHex::from_hex(&hex_string)
1067 .map_err_cli_msg("failed to decode transaction")?;
1068
1069 let client = self.client_open(&cli).await?;
1070 let tx = fedimint_core::transaction::Transaction::from_bytes(
1071 &bytes,
1072 client.decoders(),
1073 )
1074 .map_err_cli_msg("failed to decode transaction")?;
1075
1076 Ok(CliOutput::DecodeTransaction {
1077 transaction: (format!("{tx:?}")),
1078 })
1079 }
1080 },
1081 Command::Dev(DevCmd::Encode { encode_type }) => match encode_type {
1082 EncodeType::InviteCode {
1083 url,
1084 federation_id,
1085 peer,
1086 api_secret,
1087 } => Ok(CliOutput::InviteCode {
1088 invite_code: InviteCode::new(url, peer, federation_id, api_secret),
1089 }),
1090 EncodeType::Notes { notes_json } => {
1091 let notes = serde_json::from_str::<OOBNotesJson>(¬es_json)
1092 .map_err_cli_msg("invalid JSON for notes")?;
1093 let prefix =
1094 FederationIdPrefix::from_str(¬es.federation_id_prefix).map_err_cli()?;
1095 let notes = OOBNotes::new(prefix, notes.notes);
1096 Ok(CliOutput::Raw(notes.to_string().into()))
1097 }
1098 },
1099 Command::Dev(DevCmd::SessionCount) => {
1100 let client = self.client_open(&cli).await?;
1101 let count = client.api().session_count().await?;
1102 Ok(CliOutput::EpochCount { count })
1103 }
1104 Command::Dev(DevCmd::ConfigDecrypt {
1105 in_file,
1106 out_file,
1107 salt_file,
1108 password,
1109 }) => {
1110 let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&in_file));
1111 let salt = fs::read_to_string(salt_file).map_err_cli()?;
1112 let key = get_encryption_key(&password, &salt).map_err_cli()?;
1113 let decrypted_bytes = encrypted_read(&key, in_file).map_err_cli()?;
1114
1115 let mut out_file_handle = fs::File::options()
1116 .create_new(true)
1117 .write(true)
1118 .open(out_file)
1119 .expect("Could not create output cfg file");
1120 out_file_handle.write_all(&decrypted_bytes).map_err_cli()?;
1121 Ok(CliOutput::ConfigDecrypt)
1122 }
1123 Command::Dev(DevCmd::ConfigEncrypt {
1124 in_file,
1125 out_file,
1126 salt_file,
1127 password,
1128 }) => {
1129 let mut in_file_handle =
1130 fs::File::open(in_file).expect("Could not create output cfg file");
1131 let mut plaintext_bytes = vec![];
1132 in_file_handle.read_to_end(&mut plaintext_bytes).unwrap();
1133
1134 let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&out_file));
1135 let salt = fs::read_to_string(salt_file).map_err_cli()?;
1136 let key = get_encryption_key(&password, &salt).map_err_cli()?;
1137 encrypted_write(plaintext_bytes, &key, out_file).map_err_cli()?;
1138 Ok(CliOutput::ConfigEncrypt)
1139 }
1140 Command::Dev(DevCmd::ListOperationStates { operation_id }) => {
1141 #[derive(Serialize)]
1142 struct ReactorLogState {
1143 active: bool,
1144 module_instance: ModuleInstanceId,
1145 creation_time: String,
1146 #[serde(skip_serializing_if = "Option::is_none")]
1147 end_time: Option<String>,
1148 state: String,
1149 }
1150
1151 let client = self.client_open(&cli).await?;
1152
1153 let (active_states, inactive_states) =
1154 client.executor().get_operation_states(operation_id).await;
1155 let all_states =
1156 active_states
1157 .into_iter()
1158 .map(|(active_state, active_meta)| ReactorLogState {
1159 active: true,
1160 module_instance: active_state.module_instance_id(),
1161 creation_time: crate::client::time_to_iso8601(&active_meta.created_at),
1162 end_time: None,
1163 state: format!("{active_state:?}",),
1164 })
1165 .chain(inactive_states.into_iter().map(
1166 |(inactive_state, inactive_meta)| ReactorLogState {
1167 active: false,
1168 module_instance: inactive_state.module_instance_id(),
1169 creation_time: crate::client::time_to_iso8601(
1170 &inactive_meta.created_at,
1171 ),
1172 end_time: Some(crate::client::time_to_iso8601(
1173 &inactive_meta.exited_at,
1174 )),
1175 state: format!("{inactive_state:?}",),
1176 },
1177 ))
1178 .sorted_by(|a, b| a.creation_time.cmp(&b.creation_time))
1179 .collect::<Vec<_>>();
1180
1181 Ok(CliOutput::Raw(json!({
1182 "states": all_states
1183 })))
1184 }
1185 Command::Dev(DevCmd::MetaFields) => {
1186 let client = self.client_open(&cli).await?;
1187 let source = MetaModuleMetaSourceWithFallback::<LegacyMetaSource>::default();
1188
1189 let meta_fields = source
1190 .fetch(
1191 &client.config().await,
1192 &client.api_clone(),
1193 FetchKind::Initial,
1194 None,
1195 )
1196 .await
1197 .map_err_cli()?;
1198
1199 Ok(CliOutput::Raw(
1200 serde_json::to_value(meta_fields).expect("Can be encoded"),
1201 ))
1202 }
1203 Command::Dev(DevCmd::PeerVersion { peer_id }) => {
1204 let client = self.client_open(&cli).await?;
1205 let version = client
1206 .api()
1207 .fedimintd_version(peer_id.into())
1208 .await
1209 .map_err_cli()?;
1210
1211 Ok(CliOutput::Raw(json!({ "version": version })))
1212 }
1213 Command::Dev(DevCmd::ShowEventLog { pos, limit }) => {
1214 let client = self.client_open(&cli).await?;
1215
1216 let events: Vec<_> = client
1217 .get_event_log(pos, limit)
1218 .await
1219 .into_iter()
1220 .map(|v| {
1221 let module_id = v.module.as_ref().map(|m| m.1);
1222 let module_kind = v.module.map(|m| m.0);
1223 serde_json::json!({
1224 "id": v.event_id,
1225 "kind": v.event_kind,
1226 "module_kind": module_kind,
1227 "module_id": module_id,
1228 "ts": v.timestamp,
1229 "payload": v.value
1230 })
1231 })
1232 .collect();
1233
1234 Ok(CliOutput::Raw(
1235 serde_json::to_value(events).expect("Can be encoded"),
1236 ))
1237 }
1238 Command::Completion { shell } => {
1239 let bin_path = PathBuf::from(
1240 std::env::args_os()
1241 .next()
1242 .expect("Binary name is always provided if we get this far"),
1243 );
1244 let bin_name = bin_path
1245 .file_name()
1246 .expect("path has file name")
1247 .to_string_lossy();
1248 clap_complete::generate(
1249 shell,
1250 &mut Opts::command(),
1251 bin_name.as_ref(),
1252 &mut std::io::stdout(),
1253 );
1254 Ok(CliOutput::Raw(serde_json::Value::Bool(true)))
1256 }
1257 }
1258 }
1259
1260 async fn handle_admin_config_gen_command(
1261 &self,
1262 cli: Opts,
1263 config_gen_args: ConfigGenAdminArgs,
1264 ) -> anyhow::Result<Value> {
1265 let client = DynGlobalApi::from_pre_peer_id_admin_endpoint(
1266 config_gen_args.ws.clone(),
1267 &config_gen_args.api_secret,
1268 )
1269 .await?;
1270
1271 match &config_gen_args.subcommand {
1272 ConfigGenAdminCmd::ServerStatus => {
1273 let status = client.server_status(cli.auth()?).await?;
1274
1275 Ok(serde_json::to_value(status).expect("JSON serialization failed"))
1276 }
1277 ConfigGenAdminCmd::SetLocalParams {
1278 name,
1279 federation_name,
1280 } => {
1281 let info = client
1282 .set_local_params(name.clone(), federation_name.clone(), cli.auth()?)
1283 .await?;
1284
1285 Ok(serde_json::to_value(info).expect("JSON serialization failed"))
1286 }
1287 ConfigGenAdminCmd::AddPeerConnectionInfo { info } => {
1288 let name = client
1289 .add_peer_connection_info(info.clone(), cli.auth()?)
1290 .await?;
1291
1292 Ok(serde_json::to_value(name).expect("JSON serialization failed"))
1293 }
1294 ConfigGenAdminCmd::StartDkg => {
1295 client.start_dkg(cli.auth()?).await?;
1296
1297 Ok(Value::Null)
1298 }
1299 }
1300 }
1301}
1302
1303async fn log_expiration_notice(client: &Client) {
1304 client.get_meta_expiration_timestamp().await;
1305 if let Some(expiration_time) = client.get_meta_expiration_timestamp().await {
1306 match expiration_time.duration_since(fedimint_core::time::now()) {
1307 Ok(until_expiration) => {
1308 let days = until_expiration.as_secs() / (60 * 60 * 24);
1309
1310 if 90 < days {
1311 debug!(target: LOG_CLIENT, %days, "This federation will expire");
1312 } else if 30 < days {
1313 info!(target: LOG_CLIENT, %days, "This federation will expire");
1314 } else {
1315 warn!(target: LOG_CLIENT, %days, "This federation will expire soon");
1316 }
1317 }
1318 Err(_) => {
1319 tracing::error!(target: LOG_CLIENT, "This federation has expired and might not be safe to use");
1320 }
1321 }
1322 }
1323}
1324async fn print_welcome_message(client: &Client) {
1325 if let Some(welcome_message) = client
1326 .meta_service()
1327 .get_field::<String>(client.db(), "welcome_message")
1328 .await
1329 .and_then(|v| v.value)
1330 {
1331 eprintln!("{welcome_message}");
1332 }
1333}
1334
1335fn salt_from_file_path(file_path: &Path) -> PathBuf {
1336 file_path
1337 .parent()
1338 .expect("File has no parent?!")
1339 .join(SALT_FILE)
1340}
1341
1342fn metadata_from_clap_cli(metadata: Vec<String>) -> Result<BTreeMap<String, String>, CliError> {
1344 let metadata: BTreeMap<String, String> = metadata
1345 .into_iter()
1346 .map(|item| {
1347 match &item
1348 .splitn(2, '=')
1349 .map(ToString::to_string)
1350 .collect::<Vec<String>>()[..]
1351 {
1352 [] => Err(format_err!("Empty metadata argument not allowed")),
1353 [key] => Err(format_err!("Metadata {key} is missing a value")),
1354 [key, val] => Ok((key.clone(), val.clone())),
1355 [..] => unreachable!(),
1356 }
1357 })
1358 .collect::<anyhow::Result<_>>()
1359 .map_err_cli_msg("invalid metadata")?;
1360 Ok(metadata)
1361}
1362
1363#[test]
1364fn metadata_from_clap_cli_test() {
1365 for (args, expected) in [
1366 (
1367 vec!["a=b".to_string()],
1368 BTreeMap::from([("a".into(), "b".into())]),
1369 ),
1370 (
1371 vec!["a=b".to_string(), "c=d".to_string()],
1372 BTreeMap::from([("a".into(), "b".into()), ("c".into(), "d".into())]),
1373 ),
1374 ] {
1375 assert_eq!(metadata_from_clap_cli(args).unwrap(), expected);
1376 }
1377}