From 5b112c50eb326c29b2e9db8bf483cc4c638fdf07 Mon Sep 17 00:00:00 2001 From: Adam Welc Date: Thu, 24 Mar 2022 19:08:19 -0700 Subject: [PATCH] [cli] Added default Sui configuration directory (#1057) * [cli] Added default Sui configuration directory * Fixed tests * Addressed review comments * Updated docs to reflect new location of the Sui config files * Fixed a typo --- doc/src/build/wallet.md | 69 +++++++++------------ doc/src/explore/tutorials.md | 33 ++++------ sui/Cargo.toml | 1 + sui/src/sui_commands.rs | 103 ++++++++++++++++++++++++-------- sui/src/unit_tests/cli_tests.rs | 60 ++++++++++--------- sui/src/wallet.rs | 10 +++- sui/src/wallet_commands.rs | 7 ++- 7 files changed, 167 insertions(+), 116 deletions(-) diff --git a/doc/src/build/wallet.md b/doc/src/build/wallet.md index 9d928322136bf..5e2b5b24d26ba 100644 --- a/doc/src/build/wallet.md +++ b/doc/src/build/wallet.md @@ -11,21 +11,10 @@ interface, *Wallet CLI*. ## Set up -1. Follow the instructions to [install Sui binaries](install.md). - -1. Create a `sui_instance` subdirectory in your desired Sui-specific -directory. Assuming you followed recommended setup, run: - ```shell - cd "$SUI_ROOT" - mkdir sui_instance - ``` +Follow the instructions to [install Sui binaries](install.md). ## Genesis -1. Navigate to that new directory: - ```shell - cd "$SUI_ROOT"/sui_instance - ``` 1. Optionally, set `RUST_LOG=debug` for verbose logging. 1. Initiate `genesis`: ```shell @@ -41,10 +30,21 @@ arbitrarily; the process of generating the genesis state can be customized with additional accounts, objects, code, etc. as described in [Genesis customization](#customize-genesis). -The network configuration is stored in `network.conf` and -can be used subsequently to start the network. The `wallet.conf` and `wallet.key` are -also created to be used by the Sui wallet to manage the -newly created accounts. +The network configuration is stored in `network.conf` and can be used +subsequently to start the network. The `wallet.conf` and `wallet.key` +are also created to be used by the Sui wallet to manage the newly +created accounts. By default, these files will be stored in the +`~/.sui/sui_config` directory, but you can override this location by +providing an alternative path: + +```shell +sui genesis --working-dir /path/to/sui/config/files +``` +### Recreating Genesis + +To recreate Sui genesis state in the same location, which will remove +existing configuration files, use the `--force` option to the `sui +genesis` command. ## Wallet configuration The genesis process creates a configuration file `wallet.conf`, and a keystore file `wallet.key` for the @@ -123,15 +123,16 @@ implement more secure key management and support hardware signing in a future re Run the following command to start the local Sui network: ```shell -cd "$SUI_ROOT"/sui_instance sui start ``` -You can also run this command in any directory if you provide a path -to the directory where Sui configuration files are stored: +This command will by default look for Sui network coinfiguration file +(`network.conf`) in the `~/.sui/sui_config` directory, but you can +override this setting by providing a path to the directory where this +file are stored: ```shell -sui start --config "$SUI_ROOT"/sui_instance +sui start --config /path/to/sui/network/config/file ``` Executing any of these two commands in a terminal window will result @@ -140,15 +141,9 @@ instance (it will not return the command prompt). NOTE: For logs, set `RUST_LOG=debug` before invoking `sui start`. -The network config file path defaults to `./network.conf` if not -specified. - -If you see errors when trying to start Sui network, particularly if -you did not start with a fresh `"$SUI_ROOT"/sui_instance` (e.g, did -[custom wallet configuration](#wallet-configuration) or -started/restarted Sui instance multiple time), you should remove -`"$SUI_ROOT"/sui_instance` directory containing configuration files -and recreate [Sui genesis state](#genesis). +If you see errors when trying to start Sui network, particularly if you made some custom changes + (e.g, +[customized wallet configuration](#wallet-configuration)), you should [recreate Sui genesis state](#recreating-genesis). ## Using the wallet The following commands are supported by the wallet: @@ -174,20 +169,18 @@ The wallet can be started in two modes: interactive shell or command line interf To start the interactive shell, execute the following (in a different terminal window than one used to execute `sui start`): ```shell -cd "$SUI_ROOT"/sui_instance wallet ``` -You can also run this command in any directory if you provide a path -to the directory where Sui configuration files are stored: +This command will by default look for wallet coinfiguration file +(`wallet.conf`) in the `~/.sui/sui_config` directory, but you can +override this setting by providing a path to the directory where this +file are stored: ```shell -wallet --config "$SUI_ROOT"/sui_instance +wallet --config /path/to/wallet/config/file ``` -The wallet config file path defaults to `./wallet.conf` if not -specified. - The Sui interactive wallet supports the following shell functionality: * Command History The `history` command can be used to print the interactive shell's command history; @@ -206,10 +199,6 @@ The wallet can also be used without the interactive shell, which can be useful i you want to pipe the output of the wallet to another application or invoke wallet commands using scripts. -**For the remainder of this tutorial we will assume that you are -executing the `wallet` command in a directory where the Sui -configuration files are stored (`"$SUI_ROOT"/sui_instance`).** - ```shell USAGE: wallet --no-shell [SUBCOMMAND] diff --git a/doc/src/explore/tutorials.md b/doc/src/explore/tutorials.md index 632a9d35fef83..0c5de5892a15a 100644 --- a/doc/src/explore/tutorials.md +++ b/doc/src/explore/tutorials.md @@ -21,26 +21,9 @@ the `wallet` command used in the remainder of this tutorial in your path. Simply leave the terminal with Sui running and start a new terminal for the remainder of this tutorial. -We will follow the same convention as the one described in the [Sui -setup instructions](../build/wallet.md#setup) and assume that Sui -configuration files generated during Sui genesis state creation are -stored in the `"$SUI_ROOT"/sui_instance` directory. - -*IMPORTANT*: For the remainder of this tutorial, we will assume that you are -executing the `wallet` command in the `"$SUI_ROOT"/sui_instance` directory as well. -Adjust your paths accordingly. - ## Gather accounts and gas objects -After completing the [Setup section](#setup) you should have a Sui instance running in a terminal window. Now switch to a new terminal window and keep the first terminal running. -Make sure that you run the `wallet` command in the directory -where wallet configuration is located or by passing wallet's -configuration file as a parameter, as described in the `wallet` -[command description](../build/wallet.md#using-the-wallet). -This will be the same directory where you ran `sui genesis`, -so return to $SUI_ROOT/sui_instance. - -There take a look at the account addresses we own in our wallet: +Let us take a look at the account addresses we own in our wallet: ``` $ wallet --no-shell addresses Showing 5 results. @@ -93,9 +76,19 @@ export O_GAS=2110ADFB7BAF889A05EA6F5889AF7724299F9BED ``` ## Publish the TicTacToe game on Sui -We implemented a TicTacToe game in [TicTacToe.move](https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/games/sources/TicTacToe.move). To publish the game, we run the publish command and specify the path to the game package. As described in the earlier [setup section](#setup), we assume that Sui repository was cloned locally - *let us further assume that it was cloned into `"$SUI_ROOT"/sui` directory* **Adjust the `--path` to match your own environment if you used different paths**. +We implemented a TicTacToe game in [TicTacToe.move](https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/games/sources/TicTacToe.move). + +In order to obtain source code for the game, let us clone the Sui +repository to the current directory: + +```shell +git clone https://github.com/MystenLabs/sui.git +``` + +To publish the game, we run the publish command and specify the path to the source code of the game package: + ``` -$ wallet --no-shell publish --path "$SUI_ROOT"/sui/sui_programmability/examples/games --gas $ADMIN_GAS --gas-budget 30000 +$ wallet --no-shell publish --path ./sui/sui_programmability/examples/games --gas $ADMIN_GAS --gas-budget 30000 ----- Certificate ---- Signed Authorities : ... Transaction Kind : Publish diff --git a/sui/Cargo.toml b/sui/Cargo.toml index cebe2d126f48f..5a5f5505fba1d 100644 --- a/sui/Cargo.toml +++ b/sui/Cargo.toml @@ -32,6 +32,7 @@ tracing-subscriber = { version = "0.3.9", features = ["time", "registry", "env-f tracing-bunyan-formatter = "0.3" serde-value = "0.7.0" log = "0.4.14" +dirs = "4.0.0" bcs = "0.1.3" sui_core = { path = "../sui_core" } diff --git a/sui/src/sui_commands.rs b/sui/src/sui_commands.rs index f2018e359bf47..ee3ff6da7ee43 100644 --- a/sui/src/sui_commands.rs +++ b/sui/src/sui_commands.rs @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use std::collections::BTreeMap; +use std::fs; use std::path::PathBuf; use std::sync::Arc; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use futures::future::join_all; use move_binary_format::CompiledModule; use move_package::BuildConfig; @@ -29,53 +30,105 @@ use crate::config::{ use crate::gateway::{EmbeddedGatewayConfig, GatewayType}; use crate::keystore::{Keystore, KeystoreType, SuiKeystore}; +const SUI_DIR: &str = ".sui"; +const SUI_CONFIG_DIR: &str = "sui_config"; +pub const SUI_NETWORK_CONFIG: &str = "network.conf"; +pub const SUI_WALLET_CONFIG: &str = "wallet.conf"; + #[derive(StructOpt)] #[structopt(rename_all = "kebab-case")] pub enum SuiCommand { /// Start sui network. #[structopt(name = "start")] Start { - #[structopt(long, default_value = "./network.conf")] - config: PathBuf, + #[structopt(long)] + config: Option, }, #[structopt(name = "genesis")] Genesis { - #[structopt(long, default_value = ".")] - working_dir: PathBuf, #[structopt(long)] - config: Option, + working_dir: Option, + #[structopt(short, long, help = "Forces overwriting existing configuration")] + force: bool, }, } +pub fn sui_config_dir() -> Result { + match dirs::home_dir() { + Some(v) => Ok(v.join(SUI_DIR).join(SUI_CONFIG_DIR)), + None => bail!("Cannot obtain home directory path"), + } +} + impl SuiCommand { pub async fn execute(&self) -> Result<(), anyhow::Error> { match self { SuiCommand::Start { config } => { - let config: NetworkConfig = PersistedConfig::read(config)?; + let config_path = config + .clone() + .unwrap_or(sui_config_dir()?.join(SUI_NETWORK_CONFIG)); + let config: NetworkConfig = PersistedConfig::read(&config_path).map_err(|err| { + err.context(format!( + "Cannot open Sui network config file at {:?}", + config_path + )) + })?; SuiNetwork::start(&config) .await? .wait_for_completion() .await } - SuiCommand::Genesis { - working_dir, - config: path, - } => { - let network_path = working_dir.join("network.conf"); - let wallet_path = working_dir.join("wallet.conf"); - let keystore_path = working_dir.join("wallet.key"); - let db_folder_path = working_dir.join("client_db"); - - if let Ok(config) = PersistedConfig::::read(&network_path) { - if !config.authorities.is_empty() { - return Err(anyhow!("Cannot run genesis on a existing network, please delete network config file and try again.")); + SuiCommand::Genesis { working_dir, force } => { + let sui_config_dir = &match working_dir { + // if a directory is specified, it must exist (it + // will not be created) + Some(v) => v.clone(), + // create default Sui config dir if not specified + // on the command line and if it does not exist + // yet + None => { + let config_path = sui_config_dir()?; + fs::create_dir_all(&config_path)?; + config_path } - } - let genesis_conf = if let Some(path) = path { - PersistedConfig::read(path)? - } else { - GenesisConfig::default_genesis(working_dir)? }; + + // if Sui config dir is not empty then either clean it + // up (if --force/-f option was specified or report an + // error + if sui_config_dir + .read_dir() + .map_err(|err| { + anyhow!(err) + .context(format!("Cannot open Sui config dir {:?}", sui_config_dir)) + })? + .next() + .is_some() + { + if *force { + fs::remove_dir_all(sui_config_dir).map_err(|err| { + anyhow!(err).context(format!( + "Cannot remove Sui config dir {:?}", + sui_config_dir + )) + })?; + fs::create_dir(sui_config_dir).map_err(|err| { + anyhow!(err).context(format!( + "Cannot create Sui config dir {:?}", + sui_config_dir + )) + })?; + } else { + bail!("Cannot run genesis with non-empty Sui config directory {}, please use --force/-f option to remove existing configuration", sui_config_dir.to_str().unwrap()); + } + } + + let network_path = sui_config_dir.join(SUI_NETWORK_CONFIG); + let wallet_path = sui_config_dir.join(SUI_WALLET_CONFIG); + let keystore_path = sui_config_dir.join("wallet.key"); + let db_folder_path = sui_config_dir.join("client_db"); + + let genesis_conf = GenesisConfig::default_genesis(sui_config_dir)?; let (network_config, accounts, keystore) = genesis(genesis_conf).await?; info!("Network genesis completed."); let network_config = network_config.persisted(&network_path); @@ -245,7 +298,7 @@ pub async fn genesis( let package_id = generate_package_id(&mut modules, &mut genesis_ctx)?; info!("Loaded package [{}] from {:?}.", package_id, path); - // Writing package id to network.conf for user to retrieve later. + // Writing package id to network config for user to retrieve later. network_config.loaded_move_packages.push((path, package_id)); preload_modules.push(modules) } diff --git a/sui/src/unit_tests/cli_tests.rs b/sui/src/unit_tests/cli_tests.rs index d28411bc174a1..bc346eca2fbf0 100644 --- a/sui/src/unit_tests/cli_tests.rs +++ b/sui/src/unit_tests/cli_tests.rs @@ -17,7 +17,7 @@ use sui::config::{ }; use sui::gateway::{EmbeddedGatewayConfig, GatewayType}; use sui::keystore::KeystoreType; -use sui::sui_commands::{genesis, SuiNetwork}; +use sui::sui_commands::{genesis, SuiNetwork, SUI_NETWORK_CONFIG, SUI_WALLET_CONFIG}; use sui::sui_json::SuiJsonValue; use sui::wallet_commands::{WalletCommandResult, WalletCommands, WalletContext}; use sui_types::base_types::{ObjectID, SequenceNumber, SuiAddress}; @@ -51,15 +51,19 @@ macro_rules! retry_assert { async fn test_genesis() -> Result<(), anyhow::Error> { let temp_dir = tempfile::tempdir()?; let working_dir = temp_dir.path(); - let config = working_dir.join("network.conf"); + let config = working_dir.join(SUI_NETWORK_CONFIG); // Start network without authorities - let start = SuiCommand::Start { config }.execute().await; + let start = SuiCommand::Start { + config: Some(config), + } + .execute() + .await; assert!(matches!(start, Err(..))); // Genesis SuiCommand::Genesis { - working_dir: working_dir.to_path_buf(), - config: None, + working_dir: Some(working_dir.to_path_buf()), + force: false, } .execute() .await?; @@ -71,17 +75,18 @@ async fn test_genesis() -> Result<(), anyhow::Error> { .collect::>(); assert_eq!(4, files.len()); - assert!(files.contains(&"wallet.conf".to_string())); + assert!(files.contains(&SUI_WALLET_CONFIG.to_string())); assert!(files.contains(&AUTHORITIES_DB_NAME.to_string())); - assert!(files.contains(&"network.conf".to_string())); + assert!(files.contains(&SUI_NETWORK_CONFIG.to_string())); assert!(files.contains(&"wallet.key".to_string())); - // Check network.conf - let network_conf = PersistedConfig::::read(&working_dir.join("network.conf"))?; + // Check network config + let network_conf = + PersistedConfig::::read(&working_dir.join(SUI_NETWORK_CONFIG))?; assert_eq!(4, network_conf.authorities.len()); - // Check wallet.conf - let wallet_conf = PersistedConfig::::read(&working_dir.join("wallet.conf"))?; + // Check wallet config + let wallet_conf = PersistedConfig::::read(&working_dir.join(SUI_WALLET_CONFIG))?; if let GatewayType::Embedded(config) = &wallet_conf.gateway { assert_eq!(4, config.authorities.len()); @@ -94,8 +99,8 @@ async fn test_genesis() -> Result<(), anyhow::Error> { // Genesis 2nd time should fail let result = SuiCommand::Genesis { - working_dir: working_dir.to_path_buf(), - config: None, + working_dir: Some(working_dir.to_path_buf()), + force: false, } .execute() .await; @@ -119,7 +124,7 @@ async fn test_addresses_command() -> Result<(), anyhow::Error> { ..Default::default() }), }; - let wallet_conf_path = working_dir.join("wallet.conf"); + let wallet_conf_path = working_dir.join(SUI_WALLET_CONFIG); let mut wallet_config = wallet_config.persisted(&wallet_conf_path); // Add 3 accounts @@ -156,7 +161,7 @@ async fn test_cross_chain_airdrop() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context with the oracle account - let wallet_conf_path = working_dir.path().join("wallet.conf"); + let wallet_conf_path = working_dir.path().join(SUI_WALLET_CONFIG); let mut context = WalletContext::new(&wallet_conf_path)?; let recipient_address = *context.config.accounts.first().unwrap(); @@ -272,7 +277,7 @@ async fn test_objects_command() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context. - let mut context = WalletContext::new(&working_dir.path().join("wallet.conf"))?; + let mut context = WalletContext::new(&working_dir.path().join(SUI_WALLET_CONFIG))?; let address = context.config.accounts.first().cloned().unwrap(); // Sync client to retrieve objects from the network. @@ -318,7 +323,7 @@ async fn test_custom_genesis() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), Some(config)).await?; // Wallet config - let mut context = WalletContext::new(&working_dir.path().join("wallet.conf"))?; + let mut context = WalletContext::new(&working_dir.path().join(SUI_WALLET_CONFIG))?; assert_eq!(1, context.config.accounts.len()); let address = context.config.accounts.first().cloned().unwrap(); @@ -365,7 +370,8 @@ async fn test_custom_genesis_with_custom_move_package() -> Result<(), anyhow::Er assert!(logs_contain("Loading 2 Move packages")); // Checks network config contains package ids - let network_conf = PersistedConfig::::read(&working_dir.join("network.conf"))?; + let network_conf = + PersistedConfig::::read(&working_dir.join(SUI_NETWORK_CONFIG))?; assert_eq!(2, network_conf.loaded_move_packages.len()); // Make sure we log out package id @@ -374,7 +380,7 @@ async fn test_custom_genesis_with_custom_move_package() -> Result<(), anyhow::Er } // Create Wallet context. - let wallet_conf_path = working_dir.join("wallet.conf"); + let wallet_conf_path = working_dir.join(SUI_WALLET_CONFIG); let wallet_conf: WalletConfig = PersistedConfig::read(&wallet_conf_path)?; let address = *wallet_conf.accounts.last().unwrap(); let mut context = WalletContext::new(&wallet_conf_path)?; @@ -394,7 +400,7 @@ async fn test_object_info_get_command() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context. - let wallet_conf = working_dir.path().join("wallet.conf"); + let wallet_conf = working_dir.path().join(SUI_WALLET_CONFIG); let mut context = WalletContext::new(&wallet_conf)?; let address = context.config.accounts.first().cloned().unwrap(); @@ -431,7 +437,7 @@ async fn test_gas_command() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context. - let wallet_conf = working_dir.path().join("wallet.conf"); + let wallet_conf = working_dir.path().join(SUI_WALLET_CONFIG); let mut context = WalletContext::new(&wallet_conf)?; let address = context.config.accounts.first().cloned().unwrap(); let recipient = context.config.accounts.get(1).cloned().unwrap(); @@ -605,8 +611,8 @@ async fn start_test_network( genesis_config: Option, ) -> Result { let working_dir = working_dir.to_path_buf(); - let network_path = working_dir.join("network.conf"); - let wallet_path = working_dir.join("wallet.conf"); + let network_path = working_dir.join(SUI_NETWORK_CONFIG); + let wallet_path = working_dir.join(SUI_WALLET_CONFIG); let keystore_path = working_dir.join("wallet.key"); let db_folder_path = working_dir.join("client_db"); @@ -642,7 +648,7 @@ async fn start_test_network( }) .collect(); - // Create wallet.conf with stated authorities port + // Create wallet config with stated authorities port WalletConfig { accounts, keystore: KeystoreType::File(keystore_path), @@ -667,7 +673,7 @@ async fn test_move_call_args_linter_command() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context. - let wallet_conf = working_dir.path().join("wallet.conf"); + let wallet_conf = working_dir.path().join(SUI_WALLET_CONFIG); let mut context = WalletContext::new(&wallet_conf)?; let address1 = context.config.accounts.first().cloned().unwrap(); @@ -846,7 +852,7 @@ async fn test_package_publish_command() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context. - let wallet_conf = working_dir.path().join("wallet.conf"); + let wallet_conf = working_dir.path().join(SUI_WALLET_CONFIG); let mut context = WalletContext::new(&wallet_conf)?; let address = context.config.accounts.first().cloned().unwrap(); @@ -925,7 +931,7 @@ async fn test_native_transfer() -> Result<(), anyhow::Error> { let network = start_test_network(working_dir.path(), None).await?; // Create Wallet context. - let wallet_conf = working_dir.path().join("wallet.conf"); + let wallet_conf = working_dir.path().join(SUI_WALLET_CONFIG); let mut context = WalletContext::new(&wallet_conf)?; let address = context.config.accounts.first().cloned().unwrap(); diff --git a/sui/src/wallet.rs b/sui/src/wallet.rs index 26a2851695143..644b96ab8fa1a 100644 --- a/sui/src/wallet.rs +++ b/sui/src/wallet.rs @@ -16,6 +16,7 @@ use tracing_subscriber::EnvFilter; use sui::shell::{ install_shell_plugins, AsyncHandler, CacheKey, CommandStructure, CompletionCache, Shell, }; +use sui::sui_commands; use sui::wallet_commands::*; const SUI: &str = " _____ _ _ __ ____ __ @@ -35,8 +36,8 @@ struct ClientOpt { /// Run wallet command without interactive shell no_shell: bool, /// Sets the file storing the state of our user accounts (an empty one will be created if missing) - #[structopt(long, default_value = "./wallet.conf")] - config: PathBuf, + #[structopt(long)] + config: Option, /// Subcommands. Acceptable values are transfer, query_objects, benchmark, and create_accounts. #[structopt(subcommand)] cmd: Option, @@ -64,7 +65,10 @@ async fn main() -> Result<(), anyhow::Error> { let mut app: App = ClientOpt::clap(); app = app.unset_setting(AppSettings::NoBinaryName); let options: ClientOpt = ClientOpt::from_clap(&app.get_matches()); - let wallet_conf_path = options.config; + let wallet_conf_path = options + .config + .clone() + .unwrap_or(sui_commands::sui_config_dir()?.join(sui_commands::SUI_WALLET_CONFIG)); let mut context = WalletContext::new(&wallet_conf_path)?; diff --git a/sui/src/wallet_commands.rs b/sui/src/wallet_commands.rs index 0d8e57f6900c9..1e0d8d0a9555a 100644 --- a/sui/src/wallet_commands.rs +++ b/sui/src/wallet_commands.rs @@ -450,7 +450,12 @@ pub struct WalletContext { impl WalletContext { pub fn new(config_path: &Path) -> Result { - let config: WalletConfig = PersistedConfig::read(config_path)?; + let config: WalletConfig = PersistedConfig::read(config_path).map_err(|err| { + err.context(format!( + "Cannot open wallet config file at {:?}", + config_path + )) + })?; let config = config.persisted(config_path); let keystore = Arc::new(RwLock::new(config.keystore.init()?)); let gateway = config.gateway.init();