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;
14pub mod envs;
15mod utils;
16
17use core::fmt;
18use std::collections::BTreeMap;
19use std::fmt::Debug;
20use std::io::{IsTerminal, 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;
31#[cfg(feature = "tor")]
32use envs::FM_USE_TOR_ENV;
33use envs::{FM_API_SECRET_ENV, FM_DB_BACKEND_ENV, FM_IROH_ENABLE_DHT_ENV, SALT_FILE};
34use fedimint_aead::{encrypted_read, encrypted_write, get_encryption_key};
35use fedimint_api_client::api::{DynGlobalApi, FederationApiExt, FederationError};
36use fedimint_bip39::{Bip39RootSecretStrategy, Mnemonic};
37use fedimint_client::db::ApiSecretKey;
38use fedimint_client::module::meta::{FetchKind, LegacyMetaSource, MetaSource};
39use fedimint_client::module::module::init::ClientModuleInit;
40use fedimint_client::module_init::ClientModuleInitRegistry;
41use fedimint_client::secret::RootSecretStrategy;
42use fedimint_client::{AdminCreds, Client, ClientBuilder, ClientHandleArc, RootSecret};
43use fedimint_connectors::ConnectorRegistry;
44use fedimint_core::base32::FEDIMINT_PREFIX;
45use fedimint_core::config::{FederationId, FederationIdPrefix};
46use fedimint_core::core::{ModuleInstanceId, OperationId};
47use fedimint_core::db::{Database, DatabaseValue, IDatabaseTransactionOpsCoreTyped as _};
48use fedimint_core::encoding::Decodable;
49use fedimint_core::invite_code::InviteCode;
50use fedimint_core::module::{ApiAuth, ApiRequestErased};
51use fedimint_core::setup_code::PeerSetupCode;
52use fedimint_core::transaction::Transaction;
53use fedimint_core::util::{SafeUrl, backoff_util, handle_version_hash_command, retry};
54use fedimint_core::{
55 Amount, PeerId, TieredMulti, base32, fedimint_build_code_version_env, runtime,
56};
57use fedimint_eventlog::{EventLogId, EventLogTrimableId};
58use fedimint_ln_client::LightningClientInit;
59use fedimint_logging::{LOG_CLIENT, TracingSetup};
60use fedimint_meta_client::{MetaClientInit, MetaModuleMetaSourceWithFallback};
61use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, SpendableNote};
62use fedimint_wallet_client::api::WalletFederationApi;
63use fedimint_wallet_client::{WalletClientInit, WalletClientModule};
64use futures::future::pending;
65use itertools::Itertools;
66use rand::thread_rng;
67use serde::{Deserialize, Serialize};
68use serde_json::{Value, json};
69use thiserror::Error;
70use tracing::{debug, info, warn};
71use utils::parse_peer_id;
72
73use crate::client::ClientCmd;
74use crate::db::{StoredAdminCreds, load_admin_creds, store_admin_creds};
75use crate::envs::{FM_CLIENT_DIR_ENV, FM_IROH_ENABLE_NEXT_ENV, FM_OUR_ID_ENV, FM_PASSWORD_ENV};
76
77#[derive(Serialize)]
79#[serde(rename_all = "snake_case")]
80#[serde(untagged)]
81enum CliOutput {
82 VersionHash {
83 hash: String,
84 },
85
86 UntypedApiOutput {
87 value: Value,
88 },
89
90 WaitBlockCount {
91 reached: u64,
92 },
93
94 InviteCode {
95 invite_code: InviteCode,
96 },
97
98 DecodeInviteCode {
99 url: SafeUrl,
100 federation_id: FederationId,
101 },
102
103 Join {
104 joined: String,
105 },
106
107 DecodeTransaction {
108 transaction: String,
109 },
110
111 EpochCount {
112 count: u64,
113 },
114
115 ConfigDecrypt,
116
117 ConfigEncrypt,
118
119 SetupCode {
120 setup_code: PeerSetupCode,
121 },
122
123 Raw(serde_json::Value),
124}
125
126impl fmt::Display for CliOutput {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(f, "{}", serde_json::to_string_pretty(self).unwrap())
129 }
130}
131
132type CliResult<E> = Result<E, CliError>;
134
135type CliOutputResult = Result<CliOutput, CliError>;
137
138#[derive(Serialize, Error)]
140#[serde(tag = "error", rename_all(serialize = "snake_case"))]
141struct CliError {
142 error: String,
143}
144
145trait CliResultExt<O, E> {
148 fn map_err_cli(self) -> Result<O, CliError>;
150 fn map_err_cli_msg(self, msg: impl fmt::Display + Send + Sync + 'static)
152 -> Result<O, CliError>;
153}
154
155impl<O, E> CliResultExt<O, E> for result::Result<O, E>
156where
157 E: Into<anyhow::Error>,
158{
159 fn map_err_cli(self) -> Result<O, CliError> {
160 self.map_err(|e| {
161 let e = e.into();
162 CliError {
163 error: format!("{e:#}"),
164 }
165 })
166 }
167
168 fn map_err_cli_msg(
169 self,
170 msg: impl fmt::Display + Send + Sync + 'static,
171 ) -> Result<O, CliError> {
172 self.map_err(|e| Into::<anyhow::Error>::into(e))
173 .context(msg)
174 .map_err(|e| CliError {
175 error: format!("{e:#}"),
176 })
177 }
178}
179
180trait CliOptionExt<O> {
183 fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError>;
184}
185
186impl<O> CliOptionExt<O> for Option<O> {
187 fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError> {
188 self.ok_or_else(|| CliError { error: msg.into() })
189 }
190}
191
192impl From<FederationError> for CliError {
194 fn from(e: FederationError) -> Self {
195 CliError {
196 error: e.to_string(),
197 }
198 }
199}
200
201impl Debug for CliError {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 f.debug_struct("CliError")
204 .field("error", &self.error)
205 .finish()
206 }
207}
208
209impl fmt::Display for CliError {
210 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211 let json = serde_json::to_value(self).expect("CliError is valid json");
212 let json_as_string =
213 serde_json::to_string_pretty(&json).expect("valid json is serializable");
214 write!(f, "{json_as_string}")
215 }
216}
217
218#[derive(Debug, Clone, Copy, clap::ValueEnum)]
219enum DatabaseBackend {
220 #[value(name = "rocksdb")]
222 RocksDb,
223 #[value(name = "cursed-redb")]
225 CursedRedb,
226}
227
228#[derive(Parser, Clone)]
229#[command(version)]
230struct Opts {
231 #[arg(long = "data-dir", env = FM_CLIENT_DIR_ENV)]
233 data_dir: Option<PathBuf>,
234
235 #[arg(env = FM_OUR_ID_ENV, long, value_parser = parse_peer_id)]
237 our_id: Option<PeerId>,
238
239 #[arg(long, env = FM_PASSWORD_ENV)]
241 password: Option<String>,
242
243 #[cfg(feature = "tor")]
244 #[arg(long, env = FM_USE_TOR_ENV)]
246 use_tor: bool,
247
248 #[arg(long, env = FM_IROH_ENABLE_DHT_ENV)]
250 iroh_enable_dht: Option<bool>,
251
252 #[arg(long, env = FM_IROH_ENABLE_NEXT_ENV)]
254 iroh_enable_next: Option<bool>,
255
256 #[arg(long, env = FM_DB_BACKEND_ENV, value_enum, default_value = "rocksdb")]
258 db_backend: DatabaseBackend,
259
260 #[arg(short = 'v', long)]
263 verbose: bool,
264
265 #[clap(subcommand)]
266 command: Command,
267}
268
269impl Opts {
270 fn data_dir(&self) -> CliResult<&PathBuf> {
271 self.data_dir
272 .as_ref()
273 .ok_or_cli_msg("`--data-dir=` argument not set.")
274 }
275
276 async fn data_dir_create(&self) -> CliResult<&PathBuf> {
278 let dir = self.data_dir()?;
279
280 tokio::fs::create_dir_all(&dir).await.map_err_cli()?;
281
282 Ok(dir)
283 }
284 fn iroh_enable_dht(&self) -> bool {
285 self.iroh_enable_dht.unwrap_or(true)
286 }
287
288 fn iroh_enable_next(&self) -> bool {
289 self.iroh_enable_next.unwrap_or(true)
290 }
291
292 fn use_tor(&self) -> bool {
293 #[cfg(feature = "tor")]
294 return self.use_tor;
295 #[cfg(not(feature = "tor"))]
296 false
297 }
298
299 async fn admin_client(
300 &self,
301 peer_urls: &BTreeMap<PeerId, SafeUrl>,
302 api_secret: Option<&str>,
303 ) -> CliResult<DynGlobalApi> {
304 self.admin_client_with_db(peer_urls, api_secret, None).await
305 }
306
307 async fn admin_client_with_db(
308 &self,
309 peer_urls: &BTreeMap<PeerId, SafeUrl>,
310 api_secret: Option<&str>,
311 db: Option<&Database>,
312 ) -> CliResult<DynGlobalApi> {
313 let our_id = if let Some(id) = self.our_id {
315 id
316 } else if let Some(db) = db {
317 if let Some(stored_creds) = load_admin_creds(db).await {
318 stored_creds.peer_id
319 } else {
320 return Err(CliError {
321 error: "Admin client needs our-id set (no stored credentials found)"
322 .to_string(),
323 });
324 }
325 } else {
326 return Err(CliError {
327 error: "Admin client needs our-id set".to_string(),
328 });
329 };
330
331 DynGlobalApi::new_admin(
332 self.make_endpoints().await.map_err(|e| CliError {
333 error: e.to_string(),
334 })?,
335 our_id,
336 peer_urls
337 .get(&our_id)
338 .cloned()
339 .context("Our peer URL not found in config")
340 .map_err_cli()?,
341 api_secret,
342 )
343 .map_err_cli()
344 }
345
346 async fn make_endpoints(&self) -> Result<ConnectorRegistry, anyhow::Error> {
347 ConnectorRegistry::build_from_client_defaults()
348 .iroh_next(self.iroh_enable_next())
349 .iroh_pkarr_dht(self.iroh_enable_dht())
350 .ws_force_tor(self.use_tor())
351 .bind()
352 .await
353 }
354
355 fn auth(&self) -> CliResult<ApiAuth> {
356 let password = self
357 .password
358 .clone()
359 .ok_or_cli_msg("CLI needs password set")?;
360 Ok(ApiAuth(password))
361 }
362
363 async fn load_database(&self) -> CliResult<Database> {
364 debug!(target: LOG_CLIENT, "Loading client database");
365 let db_path = self.data_dir_create().await?.join("client.db");
366 match self.db_backend {
367 DatabaseBackend::RocksDb => {
368 debug!(target: LOG_CLIENT, "Using RocksDB database backend");
369 Ok(fedimint_rocksdb::RocksDb::build(db_path)
370 .open()
371 .await
372 .map_err_cli_msg("could not open rocksdb database")?
373 .into())
374 }
375 DatabaseBackend::CursedRedb => {
376 debug!(target: LOG_CLIENT, "Using CursedRedb database backend");
377 Ok(fedimint_cursed_redb::MemAndRedb::new(db_path)
378 .await
379 .map_err_cli_msg("could not open cursed redb database")?
380 .into())
381 }
382 }
383 }
384}
385
386async fn load_or_generate_mnemonic(db: &Database) -> Result<Mnemonic, CliError> {
387 Ok(
388 if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(db).await {
389 Mnemonic::from_entropy(&entropy).map_err_cli()?
390 } else {
391 debug!(
392 target: LOG_CLIENT,
393 "Generating mnemonic and writing entropy to client storage"
394 );
395 let mnemonic = Bip39RootSecretStrategy::<12>::random(&mut thread_rng());
396 Client::store_encodable_client_secret(db, mnemonic.to_entropy())
397 .await
398 .map_err_cli()?;
399 mnemonic
400 },
401 )
402}
403
404#[derive(Subcommand, Clone)]
405enum Command {
406 VersionHash,
408
409 #[clap(flatten)]
410 Client(client::ClientCmd),
411
412 #[clap(subcommand)]
413 Admin(AdminCmd),
414
415 #[clap(subcommand)]
416 Dev(DevCmd),
417
418 InviteCode {
420 peer: PeerId,
421 },
422
423 #[clap(alias = "join-federation")]
425 Join {
426 invite_code: String,
427 },
428
429 Completion {
430 shell: clap_complete::Shell,
431 },
432}
433
434#[allow(clippy::large_enum_variant)]
435#[derive(Debug, Clone, Subcommand)]
436enum AdminCmd {
437 Auth {
445 #[arg(long, env = FM_OUR_ID_ENV)]
447 peer_id: u16,
448 #[arg(long, env = FM_PASSWORD_ENV)]
450 password: String,
451 #[arg(long)]
453 no_verify: bool,
454 #[arg(long)]
456 force: bool,
457 },
458
459 Status,
461
462 Audit,
464
465 GuardianConfigBackup,
467
468 Setup(SetupAdminArgs),
469 SignApiAnnouncement {
472 api_url: SafeUrl,
474 #[clap(long)]
477 override_url: Option<SafeUrl>,
478 },
479 SignGuardianMetadata {
481 #[clap(long, value_delimiter = ',')]
483 api_urls: Vec<SafeUrl>,
484 #[clap(long)]
486 pkarr_id: String,
487 },
488 Shutdown {
490 session_idx: u64,
492 },
493 BackupStatistics,
495 ChangePassword {
498 new_password: String,
500 },
501}
502
503#[derive(Debug, Clone, Args)]
504struct SetupAdminArgs {
505 endpoint: SafeUrl,
506
507 #[clap(subcommand)]
508 subcommand: SetupAdminCmd,
509}
510
511#[derive(Debug, Clone, Subcommand)]
512enum SetupAdminCmd {
513 Status,
514 SetLocalParams {
515 name: String,
516 #[clap(long)]
517 federation_name: Option<String>,
518 #[clap(long)]
519 federation_size: Option<u32>,
520 },
521 AddPeer {
522 info: String,
523 },
524 StartDkg,
525}
526
527#[derive(Debug, Clone, Subcommand)]
528enum DecodeType {
529 InviteCode { invite_code: InviteCode },
531 #[group(required = true, multiple = false)]
533 Notes {
534 notes: Option<OOBNotes>,
536 #[arg(long)]
538 file: Option<PathBuf>,
539 },
540 Transaction { hex_string: String },
542 SetupCode { setup_code: String },
545}
546
547#[derive(Debug, Clone, Deserialize, Serialize)]
548struct OOBNotesJson {
549 federation_id_prefix: String,
550 notes: TieredMulti<SpendableNote>,
551}
552
553#[derive(Debug, Clone, Subcommand)]
554enum EncodeType {
555 InviteCode {
557 #[clap(long)]
558 url: SafeUrl,
559 #[clap(long = "federation_id")]
560 federation_id: FederationId,
561 #[clap(long = "peer")]
562 peer: PeerId,
563 #[arg(env = FM_API_SECRET_ENV)]
564 api_secret: Option<String>,
565 },
566
567 Notes { notes_json: String },
569}
570
571#[derive(Debug, Clone, Subcommand)]
572enum DevCmd {
573 #[command(after_long_help = r#"
577Examples:
578
579 fedimint-cli dev api --peer-id 0 config '"fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj"'
580 "#)]
581 Api {
582 method: String,
584 #[clap(default_value = "null")]
589 params: String,
590 #[clap(long = "peer-id")]
592 peer_id: Option<u16>,
593
594 #[clap(long = "module")]
596 module: Option<ModuleSelector>,
597
598 #[clap(long, requires = "peer_id")]
601 password: Option<String>,
602 },
603
604 ApiAnnouncements,
605
606 GuardianMetadata,
607
608 AdvanceNoteIdx {
610 #[clap(long, default_value = "1")]
611 count: usize,
612
613 #[clap(long)]
614 amount: Amount,
615 },
616
617 WaitBlockCount {
619 count: u64,
620 },
621
622 Wait {
624 seconds: Option<f32>,
626 },
627
628 WaitComplete,
630
631 Decode {
633 #[clap(subcommand)]
634 decode_type: DecodeType,
635 },
636
637 Encode {
639 #[clap(subcommand)]
640 encode_type: EncodeType,
641 },
642
643 SessionCount,
645
646 Config,
648
649 ConfigDecrypt {
650 #[arg(long = "in-file")]
652 in_file: PathBuf,
653 #[arg(long = "out-file")]
655 out_file: PathBuf,
656 #[arg(long = "salt-file")]
659 salt_file: Option<PathBuf>,
660 #[arg(env = FM_PASSWORD_ENV)]
662 password: String,
663 },
664
665 ConfigEncrypt {
666 #[arg(long = "in-file")]
668 in_file: PathBuf,
669 #[arg(long = "out-file")]
671 out_file: PathBuf,
672 #[arg(long = "salt-file")]
675 salt_file: Option<PathBuf>,
676 #[arg(env = FM_PASSWORD_ENV)]
678 password: String,
679 },
680
681 ListOperationStates {
684 operation_id: OperationId,
685 },
686 MetaFields,
690 PeerVersion {
692 #[clap(long)]
693 peer_id: u16,
694 },
695 ShowEventLog {
697 #[arg(long)]
698 pos: Option<EventLogId>,
699 #[arg(long, default_value = "10")]
700 limit: u64,
701 },
702 ShowEventLogTrimable {
704 #[arg(long)]
705 pos: Option<EventLogId>,
706 #[arg(long, default_value = "10")]
707 limit: u64,
708 },
709 TestEventLogHandling,
712 SubmitTransaction {
717 transaction: String,
719 },
720 ChainId,
723}
724
725pub struct FedimintCli {
726 module_inits: ClientModuleInitRegistry,
727 cli_args: Opts,
728}
729
730impl FedimintCli {
731 pub fn new(version_hash: &str) -> anyhow::Result<FedimintCli> {
733 assert_eq!(
734 fedimint_build_code_version_env!().len(),
735 version_hash.len(),
736 "version_hash must have an expected length"
737 );
738
739 handle_version_hash_command(version_hash);
740
741 let cli_args = Opts::parse();
742 let base_level = if cli_args.verbose { "debug" } else { "info" };
743 TracingSetup::default()
744 .with_base_level(base_level)
745 .init()
746 .expect("tracing initializes");
747
748 let version = env!("CARGO_PKG_VERSION");
749 debug!(target: LOG_CLIENT, "Starting fedimint-cli (version: {version} version_hash: {version_hash})");
750
751 Ok(Self {
752 module_inits: ClientModuleInitRegistry::new(),
753 cli_args,
754 })
755 }
756
757 pub fn with_module<T>(mut self, r#gen: T) -> Self
758 where
759 T: ClientModuleInit + 'static + Send + Sync,
760 {
761 self.module_inits.attach(r#gen);
762 self
763 }
764
765 pub fn with_default_modules(self) -> Self {
766 self.with_module(LightningClientInit::default())
767 .with_module(MintClientInit)
768 .with_module(WalletClientInit::default())
769 .with_module(MetaClientInit)
770 .with_module(fedimint_lnv2_client::LightningClientInit::default())
771 .with_module(fedimint_walletv2_client::WalletClientInit)
772 }
773
774 pub async fn run(&mut self) {
775 match self.handle_command(self.cli_args.clone()).await {
776 Ok(output) => {
777 let _ = writeln!(std::io::stdout(), "{output}");
779 }
780 Err(err) => {
781 debug!(target: LOG_CLIENT, err = %err.error.as_str(), "Command failed");
782 let _ = writeln!(std::io::stdout(), "{err}");
783 exit(1);
784 }
785 }
786 }
787
788 async fn make_client_builder(&self, cli: &Opts) -> CliResult<(ClientBuilder, Database)> {
789 let mut client_builder = Client::builder()
790 .await
791 .map_err_cli()?
792 .with_iroh_enable_dht(cli.iroh_enable_dht())
793 .with_iroh_enable_next(cli.iroh_enable_next());
794 client_builder.with_module_inits(self.module_inits.clone());
795
796 let db = cli.load_database().await?;
797 Ok((client_builder, db))
798 }
799
800 async fn client_join(
801 &mut self,
802 cli: &Opts,
803 invite_code: InviteCode,
804 ) -> CliResult<ClientHandleArc> {
805 let (client_builder, db) = self.make_client_builder(cli).await?;
806
807 let mnemonic = load_or_generate_mnemonic(&db).await?;
808
809 let client = client_builder
810 .preview(cli.make_endpoints().await.map_err_cli()?, &invite_code)
811 .await
812 .map_err_cli()?
813 .join(
814 db,
815 RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<12>::to_root_secret(
816 &mnemonic,
817 )),
818 )
819 .await
820 .map(Arc::new)
821 .map_err_cli()?;
822
823 print_welcome_message(&client).await;
824 log_expiration_notice(&client).await;
825
826 Ok(client)
827 }
828
829 async fn client_open(&self, cli: &Opts) -> CliResult<ClientHandleArc> {
830 let (mut client_builder, db) = self.make_client_builder(cli).await?;
831
832 if let Some(our_id) = cli.our_id {
834 client_builder.set_admin_creds(AdminCreds {
835 peer_id: our_id,
836 auth: cli.auth()?,
837 });
838 } else if let Some(stored_creds) = load_admin_creds(&db).await {
839 debug!(target: LOG_CLIENT, "Using stored admin credentials");
840 client_builder.set_admin_creds(AdminCreds {
841 peer_id: stored_creds.peer_id,
842 auth: ApiAuth(stored_creds.auth),
843 });
844 }
845
846 let mnemonic = Mnemonic::from_entropy(
847 &Client::load_decodable_client_secret::<Vec<u8>>(&db)
848 .await
849 .map_err_cli()?,
850 )
851 .map_err_cli()?;
852
853 let client = client_builder
854 .open(
855 cli.make_endpoints().await.map_err_cli()?,
856 db,
857 RootSecret::StandardDoubleDerive(Bip39RootSecretStrategy::<12>::to_root_secret(
858 &mnemonic,
859 )),
860 )
861 .await
862 .map(Arc::new)
863 .map_err_cli()?;
864
865 log_expiration_notice(&client).await;
866
867 Ok(client)
868 }
869
870 async fn client_recover(
871 &mut self,
872 cli: &Opts,
873 mnemonic: Mnemonic,
874 invite_code: InviteCode,
875 ) -> CliResult<ClientHandleArc> {
876 let (builder, db) = self.make_client_builder(cli).await?;
877 match Client::load_decodable_client_secret_opt::<Vec<u8>>(&db)
878 .await
879 .map_err_cli()?
880 {
881 Some(existing) => {
882 if existing != mnemonic.to_entropy() {
883 Err(anyhow::anyhow!("Previously set mnemonic does not match")).map_err_cli()?;
884 }
885 }
886 None => {
887 Client::store_encodable_client_secret(&db, mnemonic.to_entropy())
888 .await
889 .map_err_cli()?;
890 }
891 }
892
893 let root_secret = RootSecret::StandardDoubleDerive(
894 Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic),
895 );
896
897 let preview = builder
898 .preview(cli.make_endpoints().await.map_err_cli()?, &invite_code)
899 .await
900 .map_err_cli()?;
901
902 #[allow(deprecated)]
903 let backup = preview
904 .download_backup_from_federation(root_secret.clone())
905 .await
906 .map_err_cli()?;
907
908 let client = preview
909 .recover(db, root_secret, backup)
910 .await
911 .map(Arc::new)
912 .map_err_cli()?;
913
914 print_welcome_message(&client).await;
915 log_expiration_notice(&client).await;
916
917 Ok(client)
918 }
919
920 async fn handle_command(&mut self, cli: Opts) -> CliOutputResult {
921 match cli.command.clone() {
922 Command::InviteCode { peer } => {
923 let client = self.client_open(&cli).await?;
924
925 let invite_code = client
926 .invite_code(peer)
927 .await
928 .ok_or_cli_msg("peer not found")?;
929
930 Ok(CliOutput::InviteCode { invite_code })
931 }
932 Command::Join { invite_code } => {
933 {
934 let invite_code: InviteCode = InviteCode::from_str(&invite_code)
935 .map_err_cli_msg("invalid invite code")?;
936
937 let _client = self.client_join(&cli, invite_code).await?;
939 }
940
941 Ok(CliOutput::Join {
942 joined: invite_code,
943 })
944 }
945 Command::VersionHash => Ok(CliOutput::VersionHash {
946 hash: fedimint_build_code_version_env!().to_string(),
947 }),
948 Command::Client(ClientCmd::Restore {
949 mnemonic,
950 invite_code,
951 }) => {
952 let invite_code: InviteCode =
953 InviteCode::from_str(&invite_code).map_err_cli_msg("invalid invite code")?;
954 let mnemonic = Mnemonic::from_str(&mnemonic).map_err_cli()?;
955 let client = self.client_recover(&cli, mnemonic, invite_code).await?;
956
957 debug!(target: LOG_CLIENT, "Waiting for mint module recovery to finish");
960 client.wait_for_all_recoveries().await.map_err_cli()?;
961
962 debug!(target: LOG_CLIENT, "Recovery complete");
963
964 Ok(CliOutput::Raw(serde_json::to_value(()).unwrap()))
965 }
966 Command::Client(command) => {
967 let client = self.client_open(&cli).await?;
968 Ok(CliOutput::Raw(
969 client::handle_command(command, client)
970 .await
971 .map_err_cli()?,
972 ))
973 }
974 Command::Admin(AdminCmd::Auth {
975 peer_id,
976 password,
977 no_verify,
978 force,
979 }) => {
980 let db = cli.load_database().await?;
981 let peer_id = PeerId::from(peer_id);
982 let auth = ApiAuth(password);
983
984 if !force {
986 let existing = load_admin_creds(&db).await;
987 if existing.is_some() {
988 return Err(CliError {
989 error: "Admin credentials already stored. Use --force to overwrite."
990 .to_string(),
991 });
992 }
993 }
994
995 let config = Client::get_config_from_db(&db)
997 .await
998 .ok_or_cli_msg("Client not initialized. Please join a federation first.")?;
999
1000 let peer_url =
1002 config
1003 .global
1004 .api_endpoints
1005 .get(&peer_id)
1006 .ok_or_else(|| CliError {
1007 error: format!(
1008 "Peer ID {} not found in federation. Valid peer IDs are: {:?}",
1009 peer_id,
1010 config.global.api_endpoints.keys().collect::<Vec<_>>()
1011 ),
1012 })?;
1013
1014 if !no_verify {
1016 if !std::io::stdin().is_terminal() {
1018 return Err(CliError {
1019 error: "Interactive verification requires a terminal. Use --no-verify to skip.".to_string(),
1020 });
1021 }
1022
1023 eprintln!("Guardian endpoint for peer {}: {}", peer_id, peer_url.url);
1024 eprint!("Does this look correct? (y/N): ");
1025 std::io::stderr().flush().map_err_cli()?;
1026
1027 let mut input = String::new();
1028 std::io::stdin().read_line(&mut input).map_err_cli()?;
1029 let input = input.trim().to_lowercase();
1030
1031 if input != "y" && input != "yes" {
1032 return Err(CliError {
1033 error: "Endpoint verification cancelled by user.".to_string(),
1034 });
1035 }
1036 }
1037
1038 eprintln!("Verifying credentials...");
1040 let admin_api = DynGlobalApi::new_admin(
1041 cli.make_endpoints().await.map_err_cli()?,
1042 peer_id,
1043 peer_url.url.clone(),
1044 db.begin_transaction_nc()
1045 .await
1046 .get_value(&ApiSecretKey)
1047 .await
1048 .as_deref(),
1049 )
1050 .map_err_cli()?;
1051
1052 admin_api.auth(auth.clone()).await.map_err(|e| CliError {
1054 error: format!(
1055 "Failed to verify credentials: {e}. Please check your peer ID and password."
1056 ),
1057 })?;
1058
1059 store_admin_creds(
1061 &db,
1062 &StoredAdminCreds {
1063 peer_id,
1064 auth: auth.0.clone(),
1065 },
1066 )
1067 .await;
1068
1069 eprintln!("Admin credentials verified and saved successfully.");
1070 Ok(CliOutput::Raw(json!({
1071 "peer_id": peer_id,
1072 "endpoint": peer_url.url.to_string(),
1073 "status": "saved"
1074 })))
1075 }
1076 Command::Admin(AdminCmd::Audit) => {
1077 let client = self.client_open(&cli).await?;
1078
1079 let audit = cli
1080 .admin_client(
1081 &client.get_peer_urls().await,
1082 client.api_secret().as_deref(),
1083 )
1084 .await?
1085 .audit(cli.auth()?)
1086 .await?;
1087 Ok(CliOutput::Raw(
1088 serde_json::to_value(audit).map_err_cli_msg("invalid response")?,
1089 ))
1090 }
1091 Command::Admin(AdminCmd::Status) => {
1092 let client = self.client_open(&cli).await?;
1093
1094 let status = cli
1095 .admin_client_with_db(
1096 &client.get_peer_urls().await,
1097 client.api_secret().as_deref(),
1098 Some(client.db()),
1099 )
1100 .await?
1101 .status()
1102 .await?;
1103 Ok(CliOutput::Raw(
1104 serde_json::to_value(status).map_err_cli_msg("invalid response")?,
1105 ))
1106 }
1107 Command::Admin(AdminCmd::GuardianConfigBackup) => {
1108 let client = self.client_open(&cli).await?;
1109
1110 let guardian_config_backup = cli
1111 .admin_client(
1112 &client.get_peer_urls().await,
1113 client.api_secret().as_deref(),
1114 )
1115 .await?
1116 .guardian_config_backup(cli.auth()?)
1117 .await?;
1118 Ok(CliOutput::Raw(
1119 serde_json::to_value(guardian_config_backup)
1120 .map_err_cli_msg("invalid response")?,
1121 ))
1122 }
1123 Command::Admin(AdminCmd::Setup(dkg_args)) => self
1124 .handle_admin_setup_command(cli, dkg_args)
1125 .await
1126 .map(CliOutput::Raw)
1127 .map_err_cli_msg("Config Gen Error"),
1128 Command::Admin(AdminCmd::SignApiAnnouncement {
1129 api_url,
1130 override_url,
1131 }) => {
1132 let client = self.client_open(&cli).await?;
1133
1134 if !["ws", "wss"].contains(&api_url.scheme()) {
1135 return Err(CliError {
1136 error: format!(
1137 "Unsupported URL scheme {}, use ws:// or wss://",
1138 api_url.scheme()
1139 ),
1140 });
1141 }
1142
1143 let announcement = cli
1144 .admin_client(
1145 &override_url
1146 .and_then(|url| Some(vec![(cli.our_id?, url)].into_iter().collect()))
1147 .unwrap_or(client.get_peer_urls().await),
1148 client.api_secret().as_deref(),
1149 )
1150 .await?
1151 .sign_api_announcement(api_url, cli.auth()?)
1152 .await?;
1153
1154 Ok(CliOutput::Raw(
1155 serde_json::to_value(announcement).map_err_cli_msg("invalid response")?,
1156 ))
1157 }
1158 Command::Admin(AdminCmd::SignGuardianMetadata { api_urls, pkarr_id }) => {
1159 let client = self.client_open(&cli).await?;
1160
1161 let metadata = fedimint_core::net::guardian_metadata::GuardianMetadata {
1162 api_urls,
1163 pkarr_id_z32: pkarr_id,
1164 timestamp_secs: fedimint_core::time::duration_since_epoch().as_secs(),
1165 };
1166
1167 let signed_metadata = cli
1168 .admin_client(
1169 &client.get_peer_urls().await,
1170 client.api_secret().as_deref(),
1171 )
1172 .await?
1173 .sign_guardian_metadata(metadata, cli.auth()?)
1174 .await?;
1175
1176 Ok(CliOutput::Raw(
1177 serde_json::to_value(signed_metadata).map_err_cli_msg("invalid response")?,
1178 ))
1179 }
1180 Command::Admin(AdminCmd::Shutdown { session_idx }) => {
1181 let client = self.client_open(&cli).await?;
1182
1183 cli.admin_client(
1184 &client.get_peer_urls().await,
1185 client.api_secret().as_deref(),
1186 )
1187 .await?
1188 .shutdown(Some(session_idx), cli.auth()?)
1189 .await?;
1190
1191 Ok(CliOutput::Raw(json!(null)))
1192 }
1193 Command::Admin(AdminCmd::BackupStatistics) => {
1194 let client = self.client_open(&cli).await?;
1195
1196 let backup_statistics = cli
1197 .admin_client(
1198 &client.get_peer_urls().await,
1199 client.api_secret().as_deref(),
1200 )
1201 .await?
1202 .backup_statistics(cli.auth()?)
1203 .await?;
1204
1205 Ok(CliOutput::Raw(
1206 serde_json::to_value(backup_statistics).expect("Can be encoded"),
1207 ))
1208 }
1209 Command::Admin(AdminCmd::ChangePassword { new_password }) => {
1210 let client = self.client_open(&cli).await?;
1211
1212 cli.admin_client(
1213 &client.get_peer_urls().await,
1214 client.api_secret().as_deref(),
1215 )
1216 .await?
1217 .change_password(cli.auth()?, &new_password)
1218 .await?;
1219
1220 warn!(target: LOG_CLIENT, "Password changed, please restart fedimintd manually");
1221
1222 Ok(CliOutput::Raw(json!(null)))
1223 }
1224 Command::Dev(DevCmd::Api {
1225 method,
1226 params,
1227 peer_id,
1228 password: auth,
1229 module,
1230 }) => {
1231 let params = serde_json::from_str::<Value>(¶ms).unwrap_or_else(|err| {
1234 debug!(
1235 target: LOG_CLIENT,
1236 "Failed to serialize params:{}. Converting it to JSON string",
1237 err
1238 );
1239
1240 serde_json::Value::String(params)
1241 });
1242
1243 let mut params = ApiRequestErased::new(params);
1244 if let Some(auth) = auth {
1245 params = params.with_auth(ApiAuth(auth));
1246 }
1247 let client = self.client_open(&cli).await?;
1248
1249 let api = client.api_clone();
1250
1251 let module_api = match module {
1252 Some(selector) => {
1253 Some(api.with_module(selector.resolve(&client).map_err_cli()?))
1254 }
1255 None => None,
1256 };
1257
1258 let response: Value = match (peer_id, module_api) {
1259 (Some(peer_id), Some(module_api)) => module_api
1260 .request_raw(peer_id.into(), &method, ¶ms)
1261 .await
1262 .map_err_cli()?,
1263 (Some(peer_id), None) => api
1264 .request_raw(peer_id.into(), &method, ¶ms)
1265 .await
1266 .map_err_cli()?,
1267 (None, Some(module_api)) => module_api
1268 .request_current_consensus(method, params)
1269 .await
1270 .map_err_cli()?,
1271 (None, None) => api
1272 .request_current_consensus(method, params)
1273 .await
1274 .map_err_cli()?,
1275 };
1276
1277 Ok(CliOutput::UntypedApiOutput { value: response })
1278 }
1279 Command::Dev(DevCmd::AdvanceNoteIdx { count, amount }) => {
1280 let client = self.client_open(&cli).await?;
1281
1282 let mint = client
1283 .get_first_module::<MintClientModule>()
1284 .map_err_cli_msg("can't get mint module")?;
1285
1286 for _ in 0..count {
1287 mint.advance_note_idx(amount)
1288 .await
1289 .map_err_cli_msg("failed to advance the note_idx")?;
1290 }
1291
1292 Ok(CliOutput::Raw(serde_json::Value::Null))
1293 }
1294 Command::Dev(DevCmd::ApiAnnouncements) => {
1295 let client = self.client_open(&cli).await?;
1296 let announcements = client.get_peer_url_announcements().await;
1297 Ok(CliOutput::Raw(
1298 serde_json::to_value(announcements).expect("Can be encoded"),
1299 ))
1300 }
1301 Command::Dev(DevCmd::GuardianMetadata) => {
1302 let client = self.client_open(&cli).await?;
1303 let metadata = client.get_guardian_metadata().await;
1304 Ok(CliOutput::Raw(
1305 serde_json::to_value(metadata).expect("Can be encoded"),
1306 ))
1307 }
1308 Command::Dev(DevCmd::WaitBlockCount { count: target }) => retry(
1309 "wait_block_count",
1310 backoff_util::custom_backoff(
1311 Duration::from_millis(100),
1312 Duration::from_secs(5),
1313 None,
1314 ),
1315 || async {
1316 let client = self.client_open(&cli).await?;
1317 let wallet = client.get_first_module::<WalletClientModule>()?;
1318 let count = client
1319 .api()
1320 .with_module(wallet.id)
1321 .fetch_consensus_block_count()
1322 .await?;
1323 if count >= target {
1324 Ok(CliOutput::WaitBlockCount { reached: count })
1325 } else {
1326 info!(target: LOG_CLIENT, current=count, target, "Block count not reached");
1327 Err(format_err!("target not reached"))
1328 }
1329 },
1330 )
1331 .await
1332 .map_err_cli(),
1333
1334 Command::Dev(DevCmd::WaitComplete) => {
1335 let client = self.client_open(&cli).await?;
1336 client
1337 .wait_for_all_active_state_machines()
1338 .await
1339 .map_err_cli_msg("failed to wait for all active state machines")?;
1340 Ok(CliOutput::Raw(serde_json::Value::Null))
1341 }
1342 Command::Dev(DevCmd::Wait { seconds }) => {
1343 let client = self.client_open(&cli).await?;
1344 client
1348 .task_group()
1349 .spawn_cancellable("fedimint-cli dev wait: init networking", {
1350 let client = client.clone();
1351 async move {
1352 let _ = client.api().session_count().await;
1353 }
1354 });
1355
1356 if let Some(secs) = seconds {
1357 runtime::sleep(Duration::from_secs_f32(secs)).await;
1358 } else {
1359 pending::<()>().await;
1360 }
1361 Ok(CliOutput::Raw(serde_json::Value::Null))
1362 }
1363 Command::Dev(DevCmd::Decode { decode_type }) => match decode_type {
1364 DecodeType::InviteCode { invite_code } => Ok(CliOutput::DecodeInviteCode {
1365 url: invite_code.url(),
1366 federation_id: invite_code.federation_id(),
1367 }),
1368 DecodeType::Notes { notes, file } => {
1369 let notes = if let Some(notes) = notes {
1370 notes
1371 } else if let Some(file) = file {
1372 let notes_str =
1373 fs::read_to_string(file).map_err_cli_msg("failed to read file")?;
1374 OOBNotes::from_str(¬es_str).map_err_cli_msg("failed to decode notes")?
1375 } else {
1376 unreachable!("Clap enforces either notes or file being set");
1377 };
1378
1379 let notes_json = notes
1380 .notes_json()
1381 .map_err_cli_msg("failed to decode notes")?;
1382 Ok(CliOutput::Raw(notes_json))
1383 }
1384 DecodeType::Transaction { hex_string } => {
1385 let bytes: Vec<u8> = hex::FromHex::from_hex(&hex_string)
1386 .map_err_cli_msg("failed to decode transaction")?;
1387
1388 let client = self.client_open(&cli).await?;
1389 let tx = fedimint_core::transaction::Transaction::from_bytes(
1390 &bytes,
1391 client.decoders(),
1392 )
1393 .map_err_cli_msg("failed to decode transaction")?;
1394
1395 Ok(CliOutput::DecodeTransaction {
1396 transaction: (format!("{tx:?}")),
1397 })
1398 }
1399 DecodeType::SetupCode { setup_code } => {
1400 let setup_code = base32::decode_prefixed(FEDIMINT_PREFIX, &setup_code)
1401 .map_err_cli_msg("failed to decode setup code")?;
1402
1403 Ok(CliOutput::SetupCode { setup_code })
1404 }
1405 },
1406 Command::Dev(DevCmd::Encode { encode_type }) => match encode_type {
1407 EncodeType::InviteCode {
1408 url,
1409 federation_id,
1410 peer,
1411 api_secret,
1412 } => Ok(CliOutput::InviteCode {
1413 invite_code: InviteCode::new(url, peer, federation_id, api_secret),
1414 }),
1415 EncodeType::Notes { notes_json } => {
1416 let notes = serde_json::from_str::<OOBNotesJson>(¬es_json)
1417 .map_err_cli_msg("invalid JSON for notes")?;
1418 let prefix =
1419 FederationIdPrefix::from_str(¬es.federation_id_prefix).map_err_cli()?;
1420 let notes = OOBNotes::new(prefix, notes.notes);
1421 Ok(CliOutput::Raw(notes.to_string().into()))
1422 }
1423 },
1424 Command::Dev(DevCmd::SessionCount) => {
1425 let client = self.client_open(&cli).await?;
1426 let count = client.api().session_count().await?;
1427 Ok(CliOutput::EpochCount { count })
1428 }
1429 Command::Dev(DevCmd::Config) => {
1430 let client = self.client_open(&cli).await?;
1431 let config = client.get_config_json().await;
1432 Ok(CliOutput::Raw(
1433 serde_json::to_value(config).expect("Client config is serializable"),
1434 ))
1435 }
1436 Command::Dev(DevCmd::ConfigDecrypt {
1437 in_file,
1438 out_file,
1439 salt_file,
1440 password,
1441 }) => {
1442 let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&in_file));
1443 let salt = fs::read_to_string(salt_file).map_err_cli()?;
1444 let key = get_encryption_key(&password, &salt).map_err_cli()?;
1445 let decrypted_bytes = encrypted_read(&key, in_file).map_err_cli()?;
1446
1447 let mut out_file_handle = fs::File::options()
1448 .create_new(true)
1449 .write(true)
1450 .open(out_file)
1451 .expect("Could not create output cfg file");
1452 out_file_handle.write_all(&decrypted_bytes).map_err_cli()?;
1453 Ok(CliOutput::ConfigDecrypt)
1454 }
1455 Command::Dev(DevCmd::ConfigEncrypt {
1456 in_file,
1457 out_file,
1458 salt_file,
1459 password,
1460 }) => {
1461 let mut in_file_handle =
1462 fs::File::open(in_file).expect("Could not create output cfg file");
1463 let mut plaintext_bytes = vec![];
1464 in_file_handle.read_to_end(&mut plaintext_bytes).unwrap();
1465
1466 let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&out_file));
1467 let salt = fs::read_to_string(salt_file).map_err_cli()?;
1468 let key = get_encryption_key(&password, &salt).map_err_cli()?;
1469 encrypted_write(plaintext_bytes, &key, out_file).map_err_cli()?;
1470 Ok(CliOutput::ConfigEncrypt)
1471 }
1472 Command::Dev(DevCmd::ListOperationStates { operation_id }) => {
1473 #[derive(Serialize)]
1474 struct ReactorLogState {
1475 active: bool,
1476 module_instance: ModuleInstanceId,
1477 creation_time: String,
1478 #[serde(skip_serializing_if = "Option::is_none")]
1479 end_time: Option<String>,
1480 state: String,
1481 }
1482
1483 let client = self.client_open(&cli).await?;
1484
1485 let (active_states, inactive_states) =
1486 client.executor().get_operation_states(operation_id).await;
1487 let all_states =
1488 active_states
1489 .into_iter()
1490 .map(|(active_state, active_meta)| ReactorLogState {
1491 active: true,
1492 module_instance: active_state.module_instance_id(),
1493 creation_time: crate::client::time_to_iso8601(&active_meta.created_at),
1494 end_time: None,
1495 state: format!("{active_state:?}",),
1496 })
1497 .chain(inactive_states.into_iter().map(
1498 |(inactive_state, inactive_meta)| ReactorLogState {
1499 active: false,
1500 module_instance: inactive_state.module_instance_id(),
1501 creation_time: crate::client::time_to_iso8601(
1502 &inactive_meta.created_at,
1503 ),
1504 end_time: Some(crate::client::time_to_iso8601(
1505 &inactive_meta.exited_at,
1506 )),
1507 state: format!("{inactive_state:?}",),
1508 },
1509 ))
1510 .sorted_by(|a, b| a.creation_time.cmp(&b.creation_time))
1511 .collect::<Vec<_>>();
1512
1513 Ok(CliOutput::Raw(json!({
1514 "states": all_states
1515 })))
1516 }
1517 Command::Dev(DevCmd::MetaFields) => {
1518 let client = self.client_open(&cli).await?;
1519 let source = MetaModuleMetaSourceWithFallback::<LegacyMetaSource>::default();
1520
1521 let meta_fields = source
1522 .fetch(
1523 &client.config().await,
1524 &client.api_clone(),
1525 FetchKind::Initial,
1526 None,
1527 )
1528 .await
1529 .map_err_cli()?;
1530
1531 Ok(CliOutput::Raw(
1532 serde_json::to_value(meta_fields).expect("Can be encoded"),
1533 ))
1534 }
1535 Command::Dev(DevCmd::PeerVersion { peer_id }) => {
1536 let client = self.client_open(&cli).await?;
1537 let version = client
1538 .api()
1539 .fedimintd_version(peer_id.into())
1540 .await
1541 .map_err_cli()?;
1542
1543 Ok(CliOutput::Raw(json!({ "version": version })))
1544 }
1545 Command::Dev(DevCmd::ShowEventLog { pos, limit }) => {
1546 let client = self.client_open(&cli).await?;
1547
1548 let events: Vec<_> = client
1549 .get_event_log(pos, limit)
1550 .await
1551 .into_iter()
1552 .map(|v| {
1553 let id = v.id();
1554 let v = v.as_raw();
1555 let module_id = v.module.as_ref().map(|m| m.1);
1556 let module_kind = v.module.as_ref().map(|m| m.0.clone());
1557 serde_json::json!({
1558 "id": id,
1559 "kind": v.kind,
1560 "module_kind": module_kind,
1561 "module_id": module_id,
1562 "ts": v.ts_usecs,
1563 "payload": serde_json::from_slice(&v.payload).unwrap_or_else(|_| hex::encode(&v.payload)),
1564 })
1565 })
1566 .collect();
1567
1568 Ok(CliOutput::Raw(
1569 serde_json::to_value(events).expect("Can be encoded"),
1570 ))
1571 }
1572 Command::Dev(DevCmd::ShowEventLogTrimable { pos, limit }) => {
1573 let client = self.client_open(&cli).await?;
1574
1575 let events: Vec<_> = client
1576 .get_event_log_trimable(
1577 pos.map(|id| EventLogTrimableId::from(u64::from(id))),
1578 limit,
1579 )
1580 .await
1581 .into_iter()
1582 .map(|v| {
1583 let id = v.id();
1584 let v = v.as_raw();
1585 let module_id = v.module.as_ref().map(|m| m.1);
1586 let module_kind = v.module.as_ref().map(|m| m.0.clone());
1587 serde_json::json!({
1588 "id": id,
1589 "kind": v.kind,
1590 "module_kind": module_kind,
1591 "module_id": module_id,
1592 "ts": v.ts_usecs,
1593 "payload": serde_json::from_slice(&v.payload).unwrap_or_else(|_| hex::encode(&v.payload)),
1594 })
1595 })
1596 .collect();
1597
1598 Ok(CliOutput::Raw(
1599 serde_json::to_value(events).expect("Can be encoded"),
1600 ))
1601 }
1602 Command::Dev(DevCmd::SubmitTransaction { transaction }) => {
1603 let client = self.client_open(&cli).await?;
1604 let tx = Transaction::consensus_decode_hex(&transaction, client.decoders())
1605 .map_err_cli()?;
1606 let tx_outcome = client
1607 .api()
1608 .submit_transaction(tx)
1609 .await
1610 .try_into_inner(client.decoders())
1611 .map_err_cli()?;
1612
1613 Ok(CliOutput::Raw(
1614 serde_json::to_value(tx_outcome.0.map_err_cli()?).expect("Can be encoded"),
1615 ))
1616 }
1617 Command::Dev(DevCmd::TestEventLogHandling) => {
1618 let client = self.client_open(&cli).await?;
1619
1620 client
1621 .handle_events(
1622 client.built_in_application_event_log_tracker(),
1623 move |_dbtx, event| {
1624 Box::pin(async move {
1625 info!(target: LOG_CLIENT, "{event:?}");
1626
1627 Ok(())
1628 })
1629 },
1630 )
1631 .await
1632 .map_err_cli()?;
1633 unreachable!(
1634 "handle_events exits only if client shuts down, which we don't do here"
1635 )
1636 }
1637 Command::Dev(DevCmd::ChainId) => {
1638 let client = self.client_open(&cli).await?;
1639 let chain_id = client
1640 .db()
1641 .begin_transaction_nc()
1642 .await
1643 .get_value(&fedimint_client::db::ChainIdKey)
1644 .await
1645 .ok_or_cli_msg("Chain ID not cached in client database")?;
1646
1647 Ok(CliOutput::Raw(serde_json::json!({
1648 "chain_id": chain_id.to_string()
1649 })))
1650 }
1651 Command::Completion { shell } => {
1652 let bin_path = PathBuf::from(
1653 std::env::args_os()
1654 .next()
1655 .expect("Binary name is always provided if we get this far"),
1656 );
1657 let bin_name = bin_path
1658 .file_name()
1659 .expect("path has file name")
1660 .to_string_lossy();
1661 clap_complete::generate(
1662 shell,
1663 &mut Opts::command(),
1664 bin_name.as_ref(),
1665 &mut std::io::stdout(),
1666 );
1667 Ok(CliOutput::Raw(serde_json::Value::Bool(true)))
1669 }
1670 }
1671 }
1672
1673 async fn handle_admin_setup_command(
1674 &self,
1675 cli: Opts,
1676 args: SetupAdminArgs,
1677 ) -> anyhow::Result<Value> {
1678 let client =
1679 DynGlobalApi::new_admin_setup(cli.make_endpoints().await?, args.endpoint.clone())?;
1680
1681 match &args.subcommand {
1682 SetupAdminCmd::Status => {
1683 let status = client.setup_status(cli.auth()?).await?;
1684
1685 Ok(serde_json::to_value(status).expect("JSON serialization failed"))
1686 }
1687 SetupAdminCmd::SetLocalParams {
1688 name,
1689 federation_name,
1690 federation_size,
1691 } => {
1692 let info = client
1693 .set_local_params(
1694 name.clone(),
1695 federation_name.clone(),
1696 None,
1697 None,
1698 *federation_size,
1699 cli.auth()?,
1700 )
1701 .await?;
1702
1703 Ok(serde_json::to_value(info).expect("JSON serialization failed"))
1704 }
1705 SetupAdminCmd::AddPeer { info } => {
1706 let name = client
1707 .add_peer_connection_info(info.clone(), cli.auth()?)
1708 .await?;
1709
1710 Ok(serde_json::to_value(name).expect("JSON serialization failed"))
1711 }
1712 SetupAdminCmd::StartDkg => {
1713 client.start_dkg(cli.auth()?).await?;
1714
1715 Ok(Value::Null)
1716 }
1717 }
1718 }
1719}
1720
1721async fn log_expiration_notice(client: &Client) {
1722 client.get_meta_expiration_timestamp().await;
1723 if let Some(expiration_time) = client.get_meta_expiration_timestamp().await {
1724 match expiration_time.duration_since(fedimint_core::time::now()) {
1725 Ok(until_expiration) => {
1726 let days = until_expiration.as_secs() / (60 * 60 * 24);
1727
1728 if 90 < days {
1729 debug!(target: LOG_CLIENT, %days, "This federation will expire");
1730 } else if 30 < days {
1731 info!(target: LOG_CLIENT, %days, "This federation will expire");
1732 } else {
1733 warn!(target: LOG_CLIENT, %days, "This federation will expire soon");
1734 }
1735 }
1736 Err(_) => {
1737 tracing::error!(target: LOG_CLIENT, "This federation has expired and might not be safe to use");
1738 }
1739 }
1740 }
1741}
1742async fn print_welcome_message(client: &Client) {
1743 if let Some(welcome_message) = client
1744 .meta_service()
1745 .get_field::<String>(client.db(), "welcome_message")
1746 .await
1747 .and_then(|v| v.value)
1748 {
1749 eprintln!("{welcome_message}");
1750 }
1751}
1752
1753fn salt_from_file_path(file_path: &Path) -> PathBuf {
1754 file_path
1755 .parent()
1756 .expect("File has no parent?!")
1757 .join(SALT_FILE)
1758}
1759
1760fn metadata_from_clap_cli(metadata: Vec<String>) -> Result<BTreeMap<String, String>, CliError> {
1762 let metadata: BTreeMap<String, String> = metadata
1763 .into_iter()
1764 .map(|item| {
1765 match &item
1766 .splitn(2, '=')
1767 .map(ToString::to_string)
1768 .collect::<Vec<String>>()[..]
1769 {
1770 [] => Err(format_err!("Empty metadata argument not allowed")),
1771 [key] => Err(format_err!("Metadata {key} is missing a value")),
1772 [key, val] => Ok((key.clone(), val.clone())),
1773 [..] => unreachable!(),
1774 }
1775 })
1776 .collect::<anyhow::Result<_>>()
1777 .map_err_cli_msg("invalid metadata")?;
1778 Ok(metadata)
1779}
1780
1781#[test]
1782fn metadata_from_clap_cli_test() {
1783 for (args, expected) in [
1784 (
1785 vec!["a=b".to_string()],
1786 BTreeMap::from([("a".into(), "b".into())]),
1787 ),
1788 (
1789 vec!["a=b".to_string(), "c=d".to_string()],
1790 BTreeMap::from([("a".into(), "b".into()), ("c".into(), "d".into())]),
1791 ),
1792 ] {
1793 assert_eq!(metadata_from_clap_cli(args).unwrap(), expected);
1794 }
1795}