lib.rs
1 mod client; 2 mod db_locked; 3 pub mod envs; 4 mod utils; 5 6 use core::fmt; 7 use std::collections::BTreeMap; 8 use std::fmt::Debug; 9 use std::io::{Read, Write}; 10 use std::path::{Path, PathBuf}; 11 use std::process::exit; 12 use std::str::FromStr; 13 use std::sync::Arc; 14 use std::time::Duration; 15 use std::{fs, result}; 16 17 use anyhow::format_err; 18 use bip39::Mnemonic; 19 use clap::{Args, CommandFactory, Parser, Subcommand}; 20 use db_locked::LockedBuilder; 21 use fedimint_aead::{encrypted_read, encrypted_write, get_encryption_key}; 22 use fedimint_api_client::api::{ 23 DynGlobalApi, FederationApiExt, FederationError, IRawFederationApi, WsFederationApi, 24 }; 25 use fedimint_bip39::Bip39RootSecretStrategy; 26 use fedimint_client::module::init::{ClientModuleInit, ClientModuleInitRegistry}; 27 use fedimint_client::module::ClientModule as _; 28 use fedimint_client::secret::{get_default_client_secret, RootSecretStrategy}; 29 use fedimint_client::{AdminCreds, Client, ClientBuilder, ClientHandleArc}; 30 use fedimint_core::admin_client::{ConfigGenConnectionsRequest, ConfigGenParamsRequest}; 31 use fedimint_core::config::{ 32 ClientConfig, FederationId, FederationIdPrefix, ServerModuleConfigGenParamsRegistry, 33 }; 34 use fedimint_core::db::{Database, DatabaseValue}; 35 use fedimint_core::invite_code::InviteCode; 36 use fedimint_core::module::{ApiAuth, ApiRequestErased}; 37 use fedimint_core::util::{handle_version_hash_command, retry, ConstantBackoff, SafeUrl}; 38 use fedimint_core::{fedimint_build_code_version_env, runtime, PeerId, TieredMulti}; 39 use fedimint_ln_client::LightningClientInit; 40 use fedimint_logging::{TracingSetup, LOG_CLIENT}; 41 use fedimint_meta_client::MetaClientInit; 42 use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, SpendableNote}; 43 use fedimint_server::config::io::SALT_FILE; 44 use fedimint_wallet_client::api::WalletFederationApi; 45 use fedimint_wallet_client::{WalletClientInit, WalletClientModule}; 46 use futures::future::pending; 47 use rand::thread_rng; 48 use serde::{Deserialize, Serialize}; 49 use serde_json::Value; 50 use thiserror::Error; 51 use tracing::{debug, info}; 52 use utils::parse_peer_id; 53 54 use crate::client::ClientCmd; 55 use crate::envs::{FM_CLIENT_DIR_ENV, FM_OUR_ID_ENV, FM_PASSWORD_ENV}; 56 57 /// Type of output the cli produces 58 #[derive(Serialize)] 59 #[serde(rename_all = "snake_case")] 60 #[serde(untagged)] 61 enum CliOutput { 62 VersionHash { 63 hash: String, 64 }, 65 66 UntypedApiOutput { 67 value: Value, 68 }, 69 70 WaitBlockCount { 71 reached: u64, 72 }, 73 74 InviteCode { 75 invite_code: InviteCode, 76 }, 77 78 DecodeInviteCode { 79 url: SafeUrl, 80 federation_id: FederationId, 81 }, 82 83 JoinFederation { 84 joined: String, 85 }, 86 87 DecodeTransaction { 88 transaction: String, 89 }, 90 91 EpochCount { 92 count: u64, 93 }, 94 95 ConfigDecrypt, 96 97 ConfigEncrypt, 98 99 Raw(serde_json::Value), 100 } 101 102 impl fmt::Display for CliOutput { 103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 write!(f, "{}", serde_json::to_string_pretty(self).unwrap()) 105 } 106 } 107 108 /// `Result` with `CliError` as `Error` 109 type CliResult<E> = Result<E, CliError>; 110 111 /// `Result` with `CliError` as `Error` and `CliOutput` as `Ok` 112 type CliOutputResult = Result<CliOutput, CliError>; 113 114 /// Cli error 115 #[derive(Serialize, Error)] 116 #[serde(tag = "error", rename_all(serialize = "snake_case"))] 117 struct CliError { 118 error: String, 119 } 120 121 /// Extension trait making turning Results/Errors into 122 /// [`CliError`]/[`CliOutputResult`] easier 123 trait CliResultExt<O, E> { 124 /// Map error into `CliError` wrapping the original error message 125 fn map_err_cli(self) -> Result<O, CliError>; 126 /// Map error into `CliError` using custom error message `msg` 127 fn map_err_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError>; 128 } 129 130 impl<O, E> CliResultExt<O, E> for result::Result<O, E> 131 where 132 E: Into<anyhow::Error>, 133 { 134 fn map_err_cli(self) -> Result<O, CliError> { 135 self.map_err(|e| { 136 let e = e.into(); 137 CliError { 138 error: e.to_string(), 139 } 140 }) 141 } 142 143 fn map_err_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError> { 144 self.map_err(|_| CliError { error: msg.into() }) 145 } 146 } 147 148 /// Extension trait to make turning `Option`s into 149 /// [`CliError`]/[`CliOutputResult`] easier 150 trait CliOptionExt<O> { 151 fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError>; 152 } 153 154 impl<O> CliOptionExt<O> for Option<O> { 155 fn ok_or_cli_msg(self, msg: impl Into<String>) -> Result<O, CliError> { 156 self.ok_or_else(|| CliError { error: msg.into() }) 157 } 158 } 159 160 // TODO: Refactor federation API errors to just delegate to this 161 impl From<FederationError> for CliError { 162 fn from(e: FederationError) -> Self { 163 CliError { 164 error: e.to_string(), 165 } 166 } 167 } 168 169 impl Debug for CliError { 170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 171 f.debug_struct("CliError") 172 .field("error", &self.error) 173 .finish() 174 } 175 } 176 177 impl fmt::Display for CliError { 178 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 179 let json = serde_json::to_value(self).expect("CliError is valid json"); 180 let json_as_string = 181 serde_json::to_string_pretty(&json).expect("valid json is serializable"); 182 write!(f, "{}", json_as_string) 183 } 184 } 185 186 #[derive(Parser, Clone)] 187 #[command(version)] 188 struct Opts { 189 /// The working directory of the client containing the config and db 190 #[arg(long = "data-dir", env = FM_CLIENT_DIR_ENV)] 191 data_dir: Option<PathBuf>, 192 193 /// Peer id of the guardian 194 #[arg(env = FM_OUR_ID_ENV, long, value_parser = parse_peer_id)] 195 our_id: Option<PeerId>, 196 197 /// Guardian password for authentication 198 #[arg(long, env = FM_PASSWORD_ENV)] 199 password: Option<String>, 200 201 /// Activate more verbose logging, for full control use the RUST_LOG env 202 /// variable 203 #[arg(short = 'v', long)] 204 verbose: bool, 205 206 #[clap(subcommand)] 207 command: Command, 208 } 209 210 impl Opts { 211 fn data_dir(&self) -> CliResult<&PathBuf> { 212 self.data_dir 213 .as_ref() 214 .ok_or_cli_msg("`--data-dir=` argument not set.") 215 } 216 217 /// Get and create if doesn't exist the data dir 218 async fn data_dir_create(&self) -> CliResult<&PathBuf> { 219 let dir = self.data_dir()?; 220 221 tokio::fs::create_dir_all(&dir).await.map_err_cli()?; 222 223 Ok(dir) 224 } 225 226 fn admin_client(&self, cfg: &ClientConfig) -> CliResult<DynGlobalApi> { 227 let our_id = self.our_id.ok_or_cli_msg("Admin client needs our-id set")?; 228 Ok(DynGlobalApi::from_config_admin(cfg, our_id)) 229 } 230 231 fn auth(&self) -> CliResult<ApiAuth> { 232 let password = self 233 .password 234 .clone() 235 .ok_or_cli_msg("CLI needs password set")?; 236 Ok(ApiAuth(password)) 237 } 238 239 async fn load_rocks_db(&self) -> CliResult<Database> { 240 debug!(target: LOG_CLIENT, "Loading client database"); 241 let db_path = self.data_dir_create().await?.join("client.db"); 242 let lock_path = db_path.with_extension("db.lock"); 243 Ok(LockedBuilder::new(&lock_path) 244 .await 245 .map_err_cli_msg("could not lock database")? 246 .with_db( 247 fedimint_rocksdb::RocksDb::open(db_path) 248 .map_err_cli_msg("could not open database")?, 249 ) 250 .into()) 251 } 252 } 253 254 async fn load_or_generate_mnemonic(db: &Database) -> Result<Mnemonic, CliError> { 255 Ok( 256 match Client::load_decodable_client_secret::<Vec<u8>>(db).await { 257 Ok(entropy) => Mnemonic::from_entropy(&entropy).map_err_cli()?, 258 Err(_) => { 259 info!("Generating mnemonic and writing entropy to client storage"); 260 let mnemonic = Bip39RootSecretStrategy::<12>::random(&mut thread_rng()); 261 Client::store_encodable_client_secret(db, mnemonic.to_entropy()) 262 .await 263 .map_err_cli()?; 264 mnemonic 265 } 266 }, 267 ) 268 } 269 270 #[derive(Subcommand, Clone)] 271 enum Command { 272 /// Print the latest Git commit hash this bin. was built with. 273 VersionHash, 274 275 #[clap(flatten)] 276 Client(client::ClientCmd), 277 278 #[clap(subcommand)] 279 Admin(AdminCmd), 280 281 #[clap(subcommand)] 282 Dev(DevCmd), 283 284 /// Config enabling client to establish websocket connection to federation 285 InviteCode { 286 peer: PeerId, 287 }, 288 289 /// Join a federation using it's InviteCode 290 JoinFederation { 291 invite_code: String, 292 }, 293 294 Completion { 295 shell: clap_complete::Shell, 296 }, 297 } 298 299 #[derive(Debug, Clone, Subcommand)] 300 enum AdminCmd { 301 /// Show the status according to the `status` endpoint 302 Status, 303 304 /// Show an audit across all modules 305 Audit, 306 307 /// Download guardian config to back it up 308 GuardianConfigBackup, 309 310 Dkg(DkgAdminArgs), 311 } 312 313 #[derive(Debug, Clone, Args)] 314 struct DkgAdminArgs { 315 #[arg(long, env = "FM_WS_URL")] 316 ws: SafeUrl, 317 318 #[clap(subcommand)] 319 subcommand: DkgAdminCmd, 320 } 321 322 impl DkgAdminArgs { 323 fn ws_admin_client(&self) -> CliResult<DynGlobalApi> { 324 let ws = self.ws.clone(); 325 Ok(DynGlobalApi::from_pre_peer_id_admin_endpoint(ws)) 326 } 327 } 328 329 #[derive(Debug, Clone, Subcommand)] 330 enum DkgAdminCmd { 331 // These commands are roughly in the order they should be called 332 /// Allow to access the `status` endpoint in a pre-dkg phase 333 WsStatus, 334 SetPassword, 335 GetDefaultConfigGenParams, 336 SetConfigGenParams { 337 /// Guardian-defined key-value pairs that will be passed to the client 338 /// Must be a valid JSON object (Map<String, String>) 339 #[clap(long)] 340 meta_json: String, 341 /// Set the params (if leader) or just the local params (if follower) 342 #[clap(long)] 343 modules_json: String, 344 }, 345 SetConfigGenConnections { 346 /// Our guardian name 347 #[clap(long)] 348 our_name: String, 349 /// URL of "leader" guardian to send our connection info to 350 /// Will be `None` if we are the leader 351 #[clap(long)] 352 leader_api_url: Option<SafeUrl>, 353 }, 354 GetConfigGenPeers, 355 ConsensusConfigGenParams, 356 RunDkg, 357 GetVerifyConfigHash, 358 StartConsensus, 359 } 360 361 #[derive(Debug, Clone, Subcommand)] 362 enum DecodeType { 363 /// Decode an invite code string into a JSON representation 364 InviteCode { invite_code: InviteCode }, 365 /// Decode a string of ecash notes into a JSON representation 366 Notes { notes: OOBNotes }, 367 } 368 369 #[derive(Debug, Clone, Deserialize, Serialize)] 370 struct OOBNotesJson { 371 federation_id_prefix: String, 372 notes: TieredMulti<SpendableNote>, 373 } 374 375 #[derive(Debug, Clone, Subcommand)] 376 enum EncodeType { 377 /// Encode connection info from its constituent parts 378 InviteCode { 379 #[clap(long)] 380 url: SafeUrl, 381 #[clap(long = "federation_id")] 382 federation_id: FederationId, 383 #[clap(long = "peer")] 384 peer: PeerId, 385 }, 386 387 /// Encode a JSON string of notes to an ecash string 388 Notes { notes_json: String }, 389 } 390 391 #[derive(Debug, Clone, Subcommand)] 392 enum DevCmd { 393 /// Send direct method call to the API. If you specify --peer-id, it will 394 /// just ask one server, otherwise it will try to get consensus from all 395 /// servers. 396 #[command(after_long_help = r#" 397 Examples: 398 399 fedimint-cli dev api --peer-id 0 config '"fed114znk7uk7ppugdjuytr8venqf2tkywd65cqvg3u93um64tu5cw4yr0n3fvn7qmwvm4g48cpndgnm4gqq4waen5te0xyerwt3s9cczuvf6xyurzde597s7crdvsk2vmyarjw9gwyqjdzj"' 400 "#)] 401 Api { 402 /// JSON-RPC method to call 403 method: String, 404 /// JSON-RPC parameters for the request 405 /// 406 /// Note: single jsonrpc argument params string, which might require 407 /// double-quotes (see example above). 408 #[clap(default_value = "null")] 409 params: String, 410 /// Which server to send request to 411 #[clap(long = "peer-id")] 412 peer_id: Option<u16>, 413 /// Guardian password in case authenticated API endpoints are being 414 /// called. Only use together with --peer-id. 415 #[clap(long, requires = "peer_id")] 416 password: Option<String>, 417 }, 418 419 /// Wait for the fed to reach a consensus block count 420 WaitBlockCount { count: u64 }, 421 422 /// Just start the `Client` and wait 423 Wait { 424 /// Limit the wait time 425 seconds: Option<f32>, 426 }, 427 428 /// Wait for all state machines to complete 429 WaitComplete, 430 431 /// Decode invite code or ecash notes string into a JSON representation 432 Decode { 433 #[clap(subcommand)] 434 decode_type: DecodeType, 435 }, 436 437 /// Encode an invite code or ecash notes into binary 438 Encode { 439 #[clap(subcommand)] 440 encode_type: EncodeType, 441 }, 442 443 /// Gets the current fedimint AlephBFT block count 444 SessionCount, 445 446 ConfigDecrypt { 447 /// Encrypted config file 448 #[arg(long = "in-file")] 449 in_file: PathBuf, 450 /// Plaintext config file output 451 #[arg(long = "out-file")] 452 out_file: PathBuf, 453 /// Encryption salt file, otherwise defaults to the salt file from the 454 /// in_file directory 455 #[arg(long = "salt-file")] 456 salt_file: Option<PathBuf>, 457 /// The password that encrypts the configs 458 #[arg(env = FM_PASSWORD_ENV)] 459 password: String, 460 }, 461 462 ConfigEncrypt { 463 /// Plaintext config file 464 #[arg(long = "in-file")] 465 in_file: PathBuf, 466 /// Encrypted config file output 467 #[arg(long = "out-file")] 468 out_file: PathBuf, 469 /// Encryption salt file, otherwise defaults to the salt file from the 470 /// out_file directory 471 #[arg(long = "salt-file")] 472 salt_file: Option<PathBuf>, 473 /// The password that encrypts the configs 474 #[arg(env = FM_PASSWORD_ENV)] 475 password: String, 476 }, 477 478 /// Decode a transaction hex string and print it to stdout 479 DecodeTransaction { hex_string: String }, 480 } 481 482 #[derive(Debug, Serialize, Deserialize)] 483 #[serde(rename_all = "snake_case")] 484 struct PayRequest { 485 notes: TieredMulti<SpendableNote>, 486 invoice: lightning_invoice::Bolt11Invoice, 487 } 488 489 pub struct FedimintCli { 490 module_inits: ClientModuleInitRegistry, 491 cli_args: Opts, 492 } 493 494 impl FedimintCli { 495 /// Build a new `fedimintd` with a custom version hash 496 pub fn new(version_hash: &str) -> anyhow::Result<FedimintCli> { 497 assert_eq!( 498 fedimint_build_code_version_env!().len(), 499 version_hash.len(), 500 "version_hash must have an expected length" 501 ); 502 503 handle_version_hash_command(version_hash); 504 505 let cli_args = Opts::parse(); 506 let base_level = if cli_args.verbose { "info" } else { "warn" }; 507 TracingSetup::default() 508 .with_base_level(base_level) 509 .init() 510 .expect("tracing initializes"); 511 512 let version = env!("CARGO_PKG_VERSION"); 513 debug!("Starting fedimint-cli (version: {version} version_hash: {version_hash})"); 514 515 Ok(Self { 516 module_inits: ClientModuleInitRegistry::new(), 517 cli_args, 518 }) 519 } 520 521 pub fn with_module<T>(mut self, gen: T) -> Self 522 where 523 T: ClientModuleInit + 'static + Send + Sync, 524 { 525 self.module_inits.attach(gen); 526 self 527 } 528 529 pub fn with_default_modules(self) -> Self { 530 self.with_module(LightningClientInit::default()) 531 .with_module(MintClientInit) 532 .with_module(WalletClientInit::default()) 533 .with_module(MetaClientInit) 534 } 535 536 pub async fn run(&mut self) { 537 match self.handle_command(self.cli_args.clone()).await { 538 Ok(output) => { 539 // ignore if there's anyone reading the stuff we're writing out 540 let _ = writeln!(std::io::stdout(), "{output}"); 541 } 542 Err(err) => { 543 debug!(err = %err.error, "Command failed"); 544 let _ = writeln!(std::io::stdout(), "{err}"); 545 exit(1); 546 } 547 } 548 } 549 550 async fn make_client_builder(&self, cli: &Opts) -> CliResult<ClientBuilder> { 551 let db = cli.load_rocks_db().await?; 552 let mut client_builder = Client::builder(db); 553 client_builder.with_module_inits(self.module_inits.clone()); 554 client_builder.with_primary_module(1); 555 556 Ok(client_builder) 557 } 558 559 async fn client_join( 560 &mut self, 561 cli: &Opts, 562 invite_code: InviteCode, 563 ) -> CliResult<ClientHandleArc> { 564 let client_config = fedimint_api_client::download_from_invite_code(&invite_code) 565 .await 566 .map_err_cli()?; 567 568 let client_builder = self.make_client_builder(cli).await?; 569 570 let mnemonic = load_or_generate_mnemonic(client_builder.db_no_decoders()).await?; 571 572 client_builder 573 .join( 574 get_default_client_secret( 575 &Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic), 576 &client_config.global.calculate_federation_id(), 577 ), 578 client_config.clone(), 579 ) 580 .await 581 .map(Arc::new) 582 .map_err_cli() 583 } 584 585 async fn client_open(&self, cli: &Opts) -> CliResult<ClientHandleArc> { 586 let mut client_builder = self.make_client_builder(cli).await?; 587 588 if let Some(our_id) = cli.our_id { 589 client_builder.set_admin_creds(AdminCreds { 590 peer_id: our_id, 591 auth: cli.auth()?, 592 }); 593 } 594 595 let mnemonic = Mnemonic::from_entropy( 596 &Client::load_decodable_client_secret::<Vec<u8>>(client_builder.db_no_decoders()) 597 .await 598 .map_err_cli()?, 599 ) 600 .map_err_cli()?; 601 602 let config = client_builder.load_existing_config().await.map_err_cli()?; 603 604 let federation_id = config.calculate_federation_id(); 605 606 client_builder 607 .open(get_default_client_secret( 608 &Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic), 609 &federation_id, 610 )) 611 .await 612 .map(Arc::new) 613 .map_err_cli() 614 } 615 616 async fn client_recover( 617 &mut self, 618 cli: &Opts, 619 mnemonic: Mnemonic, 620 invite_code: InviteCode, 621 ) -> CliResult<ClientHandleArc> { 622 let builder = self.make_client_builder(cli).await?; 623 624 let client_config = fedimint_api_client::download_from_invite_code(&invite_code) 625 .await 626 .map_err_cli()?; 627 628 match Client::load_decodable_client_secret_opt::<Vec<u8>>(builder.db_no_decoders()) 629 .await 630 .map_err_cli()? 631 { 632 Some(existing) => { 633 if existing != mnemonic.to_entropy() { 634 Err(anyhow::anyhow!("Previously set mnemonic does not match")).map_err_cli()?; 635 } 636 } 637 None => { 638 Client::store_encodable_client_secret( 639 builder.db_no_decoders(), 640 mnemonic.to_entropy(), 641 ) 642 .await 643 .map_err_cli()?; 644 } 645 } 646 647 let root_secret = get_default_client_secret( 648 &Bip39RootSecretStrategy::<12>::to_root_secret(&mnemonic), 649 &client_config.calculate_federation_id(), 650 ); 651 let backup = builder 652 .download_backup_from_federation(&root_secret, &client_config) 653 .await 654 .map_err_cli()?; 655 builder 656 .recover(root_secret, client_config.to_owned(), backup) 657 .await 658 .map(Arc::new) 659 .map_err_cli() 660 } 661 662 async fn handle_command(&mut self, cli: Opts) -> CliOutputResult { 663 match cli.command.clone() { 664 Command::InviteCode { peer } => { 665 let db = cli.load_rocks_db().await?; 666 let client_config = Client::get_config_from_db(&db) 667 .await 668 .ok_or_cli_msg("client config code not found")?; 669 670 let invite_code = client_config 671 .invite_code(&peer) 672 .ok_or_cli_msg("peer not found")?; 673 674 Ok(CliOutput::InviteCode { invite_code }) 675 } 676 Command::JoinFederation { invite_code } => { 677 { 678 let invite_code: InviteCode = InviteCode::from_str(&invite_code) 679 .map_err_cli_msg("invalid invite code")?; 680 681 // Build client and store config in DB 682 let _client = self.client_join(&cli, invite_code).await?; 683 } 684 685 Ok(CliOutput::JoinFederation { 686 joined: invite_code, 687 }) 688 } 689 Command::VersionHash => Ok(CliOutput::VersionHash { 690 hash: fedimint_build_code_version_env!().to_string(), 691 }), 692 Command::Client(ClientCmd::Restore { 693 mnemonic, 694 invite_code, 695 }) => { 696 let invite_code: InviteCode = 697 InviteCode::from_str(&invite_code).map_err_cli_msg("invalid invite code")?; 698 let mnemonic = Mnemonic::from_str(&mnemonic).map_err_cli()?; 699 let client = self.client_recover(&cli, mnemonic, invite_code).await?; 700 701 // TODO: until we implement recovery for other modules we can't really wait 702 // for more than this one 703 debug!("Waiting for mint module recovery to finish"); 704 client 705 .wait_for_module_kind_recovery(MintClientModule::kind()) 706 .await 707 .map_err_cli()?; 708 709 debug!("Recovery complete"); 710 711 Ok(CliOutput::Raw(serde_json::to_value(()).unwrap())) 712 } 713 Command::Client(command) => { 714 let client = self.client_open(&cli).await?; 715 Ok(CliOutput::Raw( 716 client::handle_command(command, client) 717 .await 718 .map_err_cli()?, 719 )) 720 } 721 Command::Admin(AdminCmd::Audit) => { 722 let client = self.client_open(&cli).await?; 723 724 let audit = cli 725 .admin_client(client.get_config())? 726 .audit(cli.auth()?) 727 .await?; 728 Ok(CliOutput::Raw( 729 serde_json::to_value(audit).map_err_cli_msg("invalid response")?, 730 )) 731 } 732 Command::Admin(AdminCmd::Status) => { 733 let client = self.client_open(&cli).await?; 734 735 let status = cli.admin_client(client.get_config())?.status().await?; 736 Ok(CliOutput::Raw( 737 serde_json::to_value(status).map_err_cli_msg("invalid response")?, 738 )) 739 } 740 Command::Admin(AdminCmd::GuardianConfigBackup) => { 741 let client = self.client_open(&cli).await?; 742 743 let guardian_config_backup = cli 744 .admin_client(client.get_config())? 745 .guardian_config_backup(cli.auth()?) 746 .await?; 747 Ok(CliOutput::Raw( 748 serde_json::to_value(guardian_config_backup) 749 .map_err_cli_msg("invalid response")?, 750 )) 751 } 752 Command::Admin(AdminCmd::Dkg(dkg_args)) => { 753 self.handle_admin_dkg_command(cli, dkg_args).await 754 } 755 Command::Dev(DevCmd::Api { 756 method, 757 params, 758 peer_id, 759 password: auth, 760 }) => { 761 //Parse params to JSON. 762 //If fails, convert to JSON string. 763 let params = serde_json::from_str::<Value>(¶ms).unwrap_or_else(|err| { 764 debug!( 765 "Failed to serialize params:{}. Converting it to JSON string", 766 err 767 ); 768 769 serde_json::Value::String(params) 770 }); 771 772 let mut params = ApiRequestErased::new(params); 773 if let Some(auth) = auth { 774 params = params.with_auth(ApiAuth(auth)) 775 } 776 let client = self.client_open(&cli).await?; 777 778 let ws_api: Arc<_> = WsFederationApi::from_config(client.get_config()).into(); 779 let response: Value = match peer_id { 780 Some(peer_id) => ws_api 781 .request_raw(peer_id.into(), &method, &[params.to_json()]) 782 .await 783 .map_err_cli()?, 784 None => ws_api 785 .request_current_consensus(method, params) 786 .await 787 .map_err_cli()?, 788 }; 789 790 Ok(CliOutput::UntypedApiOutput { value: response }) 791 } 792 Command::Dev(DevCmd::WaitBlockCount { count: target }) => retry( 793 "wait_block_count", 794 ConstantBackoff::default() 795 .with_delay(Duration::from_millis(100)) 796 .with_max_times(usize::MAX), 797 || async { 798 let client = self.client_open(&cli).await?; 799 let wallet = client.get_first_module::<WalletClientModule>(); 800 let count = client 801 .api() 802 .with_module(wallet.id) 803 .fetch_consensus_block_count() 804 .await?; 805 if count >= target { 806 Ok(CliOutput::WaitBlockCount { reached: count }) 807 } else { 808 info!(target: LOG_CLIENT, current=count, target, "Block count not reached"); 809 Err(format_err!("target not reached")) 810 } 811 }, 812 ) 813 .await 814 .map_err_cli(), 815 816 Command::Dev(DevCmd::WaitComplete) => { 817 let client = self.client_open(&cli).await?; 818 client 819 .wait_for_all_active_state_machines() 820 .await 821 .map_err_cli_msg("failed to wait for all active state machines")?; 822 Ok(CliOutput::Raw(serde_json::Value::Null)) 823 } 824 Command::Dev(DevCmd::Wait { seconds }) => { 825 let _client = self.client_open(&cli).await?; 826 if let Some(secs) = seconds { 827 runtime::sleep(Duration::from_secs_f32(secs)).await 828 } else { 829 pending().await 830 } 831 Ok(CliOutput::Raw(serde_json::Value::Null)) 832 } 833 Command::Dev(DevCmd::Decode { decode_type }) => match decode_type { 834 DecodeType::InviteCode { invite_code } => Ok(CliOutput::DecodeInviteCode { 835 url: invite_code.url(), 836 federation_id: invite_code.federation_id(), 837 }), 838 DecodeType::Notes { notes } => { 839 let notes_json = notes 840 .notes_json() 841 .map_err_cli_msg("failed to decode notes")?; 842 Ok(CliOutput::Raw(notes_json)) 843 } 844 }, 845 Command::Dev(DevCmd::Encode { encode_type }) => match encode_type { 846 EncodeType::InviteCode { 847 url, 848 federation_id, 849 peer, 850 } => Ok(CliOutput::InviteCode { 851 invite_code: InviteCode::new(url, peer, federation_id), 852 }), 853 EncodeType::Notes { notes_json } => { 854 let notes = serde_json::from_str::<OOBNotesJson>(¬es_json) 855 .map_err_cli_msg("invalid JSON for notes")?; 856 let prefix = 857 FederationIdPrefix::from_str(¬es.federation_id_prefix).map_err_cli()?; 858 let notes = OOBNotes::new(prefix, notes.notes); 859 Ok(CliOutput::Raw(notes.to_string().into())) 860 } 861 }, 862 Command::Dev(DevCmd::SessionCount) => { 863 let client = self.client_open(&cli).await?; 864 let count = client.api().session_count().await?; 865 Ok(CliOutput::EpochCount { count }) 866 } 867 Command::Dev(DevCmd::ConfigDecrypt { 868 in_file, 869 out_file, 870 salt_file, 871 password, 872 }) => { 873 let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&in_file)); 874 let salt = fs::read_to_string(salt_file).map_err_cli()?; 875 let key = get_encryption_key(&password, &salt).map_err_cli()?; 876 let decrypted_bytes = encrypted_read(&key, in_file).map_err_cli()?; 877 878 let mut out_file_handle = fs::File::options() 879 .create_new(true) 880 .write(true) 881 .open(out_file) 882 .expect("Could not create output cfg file"); 883 out_file_handle.write_all(&decrypted_bytes).map_err_cli()?; 884 Ok(CliOutput::ConfigDecrypt) 885 } 886 Command::Dev(DevCmd::ConfigEncrypt { 887 in_file, 888 out_file, 889 salt_file, 890 password, 891 }) => { 892 let mut in_file_handle = 893 fs::File::open(in_file).expect("Could not create output cfg file"); 894 let mut plaintext_bytes = vec![]; 895 in_file_handle.read_to_end(&mut plaintext_bytes).unwrap(); 896 897 let salt_file = salt_file.unwrap_or_else(|| salt_from_file_path(&out_file)); 898 let salt = fs::read_to_string(salt_file).map_err_cli()?; 899 let key = get_encryption_key(&password, &salt).map_err_cli()?; 900 encrypted_write(plaintext_bytes, &key, out_file).map_err_cli()?; 901 Ok(CliOutput::ConfigEncrypt) 902 } 903 Command::Dev(DevCmd::DecodeTransaction { hex_string }) => { 904 let bytes: Vec<u8> = hex::FromHex::from_hex(&hex_string) 905 .map_err_cli_msg("failed to decode transaction")?; 906 907 let client = self.client_open(&cli).await?; 908 let tx = 909 fedimint_core::transaction::Transaction::from_bytes(&bytes, client.decoders()) 910 .map_err_cli_msg("failed to decode transaction")?; 911 912 Ok(CliOutput::DecodeTransaction { 913 transaction: (format!("{tx:?}")), 914 }) 915 } 916 Command::Completion { shell } => { 917 clap_complete::generate( 918 shell, 919 &mut Opts::command(), 920 "fedimint-cli", 921 &mut std::io::stdout(), 922 ); 923 // HACK: prints true to stdout which is fine for shells 924 Ok(CliOutput::Raw(serde_json::Value::Bool(true))) 925 } 926 } 927 } 928 929 async fn handle_admin_dkg_command( 930 &self, 931 cli: Opts, 932 dkg_args: DkgAdminArgs, 933 ) -> Result<CliOutput, CliError> { 934 let client = dkg_args.ws_admin_client()?; 935 match &dkg_args.subcommand { 936 DkgAdminCmd::WsStatus => { 937 let status = client.status().await?; 938 Ok(CliOutput::Raw( 939 serde_json::to_value(status).map_err_cli_msg("invalid response")?, 940 )) 941 } 942 DkgAdminCmd::SetPassword => { 943 client.set_password(cli.auth()?).await?; 944 Ok(CliOutput::Raw(Value::Null)) 945 } 946 DkgAdminCmd::GetDefaultConfigGenParams => { 947 let default_params = client.get_default_config_gen_params(cli.auth()?).await?; 948 Ok(CliOutput::Raw( 949 serde_json::to_value(default_params).map_err_cli_msg("invalid response")?, 950 )) 951 } 952 DkgAdminCmd::SetConfigGenParams { 953 meta_json, 954 modules_json, 955 } => { 956 let meta: BTreeMap<String, String> = 957 serde_json::from_str(meta_json).map_err_cli_msg("Invalid JSON")?; 958 let modules: ServerModuleConfigGenParamsRegistry = 959 serde_json::from_str(modules_json).map_err_cli_msg("Invalid JSON")?; 960 let params = ConfigGenParamsRequest { meta, modules }; 961 client.set_config_gen_params(params, cli.auth()?).await?; 962 Ok(CliOutput::Raw(Value::Null)) 963 } 964 DkgAdminCmd::SetConfigGenConnections { 965 our_name, 966 leader_api_url, 967 } => { 968 let req = ConfigGenConnectionsRequest { 969 our_name: our_name.to_owned(), 970 leader_api_url: leader_api_url.to_owned(), 971 }; 972 client.set_config_gen_connections(req, cli.auth()?).await?; 973 Ok(CliOutput::Raw(Value::Null)) 974 } 975 DkgAdminCmd::GetConfigGenPeers => { 976 let peer_server_params = client.get_config_gen_peers().await?; 977 Ok(CliOutput::Raw( 978 serde_json::to_value(peer_server_params).map_err_cli_msg("invalid response")?, 979 )) 980 } 981 DkgAdminCmd::ConsensusConfigGenParams => { 982 let config_gen_params_response = client.consensus_config_gen_params().await?; 983 Ok(CliOutput::Raw( 984 serde_json::to_value(config_gen_params_response) 985 .map_err_cli_msg("invalid response")?, 986 )) 987 } 988 DkgAdminCmd::RunDkg => { 989 client.run_dkg(cli.auth()?).await?; 990 Ok(CliOutput::Raw(Value::Null)) 991 } 992 DkgAdminCmd::GetVerifyConfigHash => { 993 let hashes_by_peer = client.get_verify_config_hash(cli.auth()?).await?; 994 Ok(CliOutput::Raw( 995 serde_json::to_value(hashes_by_peer).map_err_cli_msg("invalid response")?, 996 )) 997 } 998 DkgAdminCmd::StartConsensus => { 999 client.start_consensus(cli.auth()?).await?; 1000 Ok(CliOutput::Raw(Value::Null)) 1001 } 1002 } 1003 } 1004 } 1005 1006 fn salt_from_file_path(file_path: &Path) -> PathBuf { 1007 file_path 1008 .parent() 1009 .expect("File has no parent?!") 1010 .join(SALT_FILE) 1011 } 1012 1013 /// Convert clap arguments to backup metadata 1014 fn metadata_from_clap_cli(metadata: Vec<String>) -> Result<BTreeMap<String, String>, CliError> { 1015 let metadata: BTreeMap<String, String> = metadata 1016 .into_iter() 1017 .map(|item| { 1018 match &item 1019 .splitn(2, '=') 1020 .map(ToString::to_string) 1021 .collect::<Vec<String>>()[..] 1022 { 1023 [] => Err(format_err!("Empty metadata argument not allowed")), 1024 [key] => Err(format_err!("Metadata {key} is missing a value")), 1025 [key, val] => Ok((key.clone(), val.clone())), 1026 [..] => unreachable!(), 1027 } 1028 }) 1029 .collect::<anyhow::Result<_>>() 1030 .map_err_cli_msg("invalid metadata")?; 1031 Ok(metadata) 1032 } 1033 1034 #[test] 1035 fn metadata_from_clap_cli_test() { 1036 for (args, expected) in [ 1037 ( 1038 vec!["a=b".to_string()], 1039 BTreeMap::from([("a".into(), "b".into())]), 1040 ), 1041 ( 1042 vec!["a=b".to_string(), "c=d".to_string()], 1043 BTreeMap::from([("a".into(), "b".into()), ("c".into(), "d".into())]), 1044 ), 1045 ] { 1046 assert_eq!(metadata_from_clap_cli(args).unwrap(), expected); 1047 } 1048 }