From efb429a0d04f8ff77445d8fd066f445f6531ab06 Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Thu, 22 Sep 2022 01:29:18 -0700 Subject: [PATCH] [aptos-cli] Use named networks There's confusion between the faucet and network endpoints. This just adds prenamed combinations that can be used, while leaving the ability for custom combinations. Additionally, stops making the init fail if the faucet fails. --- crates/aptos/src/common/init.rs | 258 +++++++++++++++++++++++-------- crates/aptos/src/common/types.rs | 3 + crates/aptos/src/test/mod.rs | 3 +- 3 files changed, 201 insertions(+), 63 deletions(-) diff --git a/crates/aptos/src/common/init.rs b/crates/aptos/src/common/init.rs index 71d6ed706a44b..ee4948ee6f9fe 100644 --- a/crates/aptos/src/common/init.rs +++ b/crates/aptos/src/common/init.rs @@ -11,12 +11,16 @@ use crate::common::{ utils::{fund_account, prompt_yes_with_override, read_line}, }; use aptos_crypto::{ed25519::Ed25519PrivateKey, PrivateKey, ValidCryptoMaterialStringExt}; +use aptos_rest_client::aptos_api_types::{AptosError, AptosErrorCode}; +use aptos_rest_client::error::{AptosErrorResponse, RestError}; use async_trait::async_trait; use clap::Parser; use reqwest::Url; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::str::FromStr; -pub const DEFAULT_REST_URL: &str = "https://fullnode.devnet.aptoslabs.com/v1"; +pub const DEFAULT_REST_URL: &str = "https://fullnode.devnet.aptoslabs.com"; pub const DEFAULT_FAUCET_URL: &str = "https://faucet.devnet.aptoslabs.com"; const NUM_DEFAULT_COINS: u64 = 10000; @@ -25,6 +29,12 @@ const NUM_DEFAULT_COINS: u64 = 10000; /// Configuration will be pushed into .aptos/config.yaml #[derive(Debug, Parser)] pub struct InitTool { + /// Network to use for default settings + /// + /// If custom `rest_url` and `faucet_url` are wanted, use `custom` + #[clap(long)] + pub network: Option, + /// URL to a fullnode on the network #[clap(long)] pub rest_url: Option, @@ -77,66 +87,37 @@ impl CliCommand<()> for InitTool { eprintln!("Configuring for profile {}", profile_name); - // Rest Endpoint - let rest_url = if let Some(rest_url) = self.rest_url { - eprintln!("Using command line argument for rest URL {}", rest_url); - rest_url + // Choose a network + // TODO: Change custom to a specific network int he future + eprintln!("Choose network from [testnet, devnet, local, custom | defaults to custom]"); + let input = read_line("network")?; + let input = input.trim(); + let network = if input.is_empty() { + eprintln!("No network given, using devnet..."); + Network::Custom } else { - eprintln!( - "Enter your rest endpoint [Current: {} | No input: {}]", - profile_config - .rest_url - .unwrap_or_else(|| "None".to_string()), - DEFAULT_REST_URL - ); - let input = read_line("Rest endpoint")?; - let input = input.trim(); - if input.is_empty() { - eprintln!("No rest url given, using {}...", DEFAULT_REST_URL); - reqwest::Url::parse(DEFAULT_REST_URL).map_err(|err| { - CliError::UnexpectedError(format!("Failed to parse default rest URL {}", err)) - })? - } else { - reqwest::Url::parse(input) - .map_err(|err| CliError::UnableToParse("Rest Endpoint", err.to_string()))? - } + Network::from_str(input)? }; - profile_config.rest_url = Some(rest_url.to_string()); - // Faucet Endpoint - let faucet_url = if self.skip_faucet { - eprintln!("Not configuring a faucet because --skip-faucet was provided"); - None - } else if let Some(faucet_url) = self.faucet_url { - eprintln!("Using command line argument for faucet URL {}", faucet_url); - Some(faucet_url) - } else { - eprintln!( - "Enter your faucet endpoint [Current: {} | No input: {} | 'skip' to not use a faucet]", - profile_config - .faucet_url - .unwrap_or_else(|| "None".to_string()), - DEFAULT_FAUCET_URL - ); - let input = read_line("Faucet endpoint")?; - let input = input.trim(); - if input.is_empty() { - eprintln!("No faucet url given, using {}...", DEFAULT_FAUCET_URL); - Some(reqwest::Url::parse(DEFAULT_FAUCET_URL).map_err(|err| { - CliError::UnexpectedError(format!("Failed to parse default faucet URL {}", err)) - })?) - } else if input.to_lowercase() == "skip" { - eprintln!("Skipping faucet"); - None - } else { - Some( - reqwest::Url::parse(input).map_err(|err| { - CliError::UnableToParse("Faucet Endpoint", err.to_string()) - })?, - ) + let mut is_community_faucet = false; + + match network { + Network::Testnet => { + profile_config.rest_url = + Some("https://fullnode.testnet.aptoslabs.com".to_string()); + profile_config.faucet_url = None; + is_community_faucet = true; } - }; - profile_config.faucet_url = faucet_url.as_ref().map(|inner| inner.to_string()); + Network::Devnet => { + profile_config.rest_url = Some("https://fullnode.devnet.aptoslabs.com".to_string()); + profile_config.faucet_url = Some("https://faucet.devnet.aptoslabs.com".to_string()); + } + Network::Local => { + profile_config.rest_url = Some("http://localhost:8080".to_string()); + profile_config.faucet_url = Some("http://localhost:8081".to_string()); + } + Network::Custom => self.custom_network(&mut profile_config)?, + } // Private key let private_key = if let Some(private_key) = self @@ -171,15 +152,67 @@ impl CliCommand<()> for InitTool { profile_config.account = Some(address); // Create account if it doesn't exist (and there's a faucet) - let client = aptos_rest_client::Client::new(rest_url); - if let Some(faucet_url) = faucet_url { - if client.get_account(address).await.is_err() { + let client = aptos_rest_client::Client::new( + Url::parse(profile_config.rest_url.as_ref().unwrap()) + .map_err(|err| CliError::UnableToParse("rest_url", err.to_string()))?, + ); + + // Check if account exists + let account_exists = match client.get_account(address).await { + Ok(_) => true, + Err(err) => { + if let RestError::Api(AptosErrorResponse { + error: + AptosError { + error_code: AptosErrorCode::ResourceNotFound, + .. + }, + .. + }) + | RestError::Api(AptosErrorResponse { + error: + AptosError { + error_code: AptosErrorCode::AccountNotFound, + .. + }, + .. + }) = err + { + false + } else { + return Err(CliError::UnexpectedError(format!( + "Failed to check if account exists: {:?}", + err + ))); + } + } + }; + if let Some(ref faucet_url) = profile_config.faucet_url { + if account_exists { + eprintln!("Account {} has been already found onchain", address); + } else { eprintln!( "Account {} doesn't exist, creating it and funding it with {} Octas", address, NUM_DEFAULT_COINS ); - fund_account(faucet_url, NUM_DEFAULT_COINS, address).await?; + match fund_account( + Url::parse(faucet_url) + .map_err(|err| CliError::UnableToParse("rest_url", err.to_string()))?, + NUM_DEFAULT_COINS, + address, + ) + .await + { + Ok(_) => eprintln!("Account {} funded successfully", address), + Err(err) => eprintln!("Account {} failed to be funded: {:?}", address, err), + }; } + } else if account_exists { + eprintln!("Account {} has been already found onchain", address); + } else if is_community_faucet { + eprintln!("Account {} does not exist, you may need to fund the account through a community faucet e.g. https://aptoslabs.com/testnet-faucet", address); + } else { + eprintln!("Account {} has been initialized locally, but must have coins transferred to it to create the account onchain", address); } // Ensure the loaded config has profiles setup for a possible empty file @@ -192,7 +225,108 @@ impl CliCommand<()> for InitTool { .unwrap() .insert(profile_name.to_string(), profile_config); config.save()?; - eprintln!("Aptos is now set up for account {}! Run `aptos help` for more information about commands", address); + eprintln!("\n---\nAptos CLI is now set up for account {} as profile {}! Run `aptos --help` for more information about commands", address, self.profile_options.profile_name().unwrap_or(DEFAULT_PROFILE)); + Ok(()) + } +} + +impl InitTool { + fn custom_network(&self, profile_config: &mut ProfileConfig) -> CliTypedResult<()> { + // Rest Endpoint + let rest_url = if let Some(ref rest_url) = self.rest_url { + eprintln!("Using command line argument for rest URL {}", rest_url); + rest_url.clone() + } else { + eprintln!( + "Enter your rest endpoint [Current: {} | No input: {}]", + profile_config.rest_url.as_deref().unwrap_or("None"), + DEFAULT_REST_URL + ); + let input = read_line("Rest endpoint")?; + let input = input.trim(); + if input.is_empty() { + eprintln!("No rest url given, using {}...", DEFAULT_REST_URL); + reqwest::Url::parse(DEFAULT_REST_URL).map_err(|err| { + CliError::UnexpectedError(format!("Failed to parse default rest URL {}", err)) + })? + } else { + reqwest::Url::parse(input) + .map_err(|err| CliError::UnableToParse("Rest Endpoint", err.to_string()))? + } + }; + profile_config.rest_url = Some(rest_url.to_string()); + + // Faucet Endpoint + let faucet_url = if self.skip_faucet { + eprintln!("Not configuring a faucet because --skip-faucet was provided"); + None + } else if let Some(ref faucet_url) = self.faucet_url { + eprintln!("Using command line argument for faucet URL {}", faucet_url); + Some(faucet_url.clone()) + } else { + eprintln!( + "Enter your faucet endpoint [Current: {} | No input: {} | 'skip' to not use a faucet]", + profile_config + .faucet_url.as_deref() + .unwrap_or("None"), + DEFAULT_FAUCET_URL + ); + let input = read_line("Faucet endpoint")?; + let input = input.trim(); + if input.is_empty() { + eprintln!("No faucet url given, using {}...", DEFAULT_FAUCET_URL); + Some(reqwest::Url::parse(DEFAULT_FAUCET_URL).map_err(|err| { + CliError::UnexpectedError(format!("Failed to parse default faucet URL {}", err)) + })?) + } else if input.to_lowercase() == "skip" { + eprintln!("Skipping faucet"); + None + } else { + Some( + reqwest::Url::parse(input).map_err(|err| { + CliError::UnableToParse("Faucet Endpoint", err.to_string()) + })?, + ) + } + }; + profile_config.faucet_url = faucet_url.as_ref().map(|inner| inner.to_string()); Ok(()) } } + +/// A simplified list of all networks supported by the CLI +/// +/// Any command using this, will be simpler to setup as profiles +#[derive(Debug, Serialize, Deserialize)] +pub enum Network { + Testnet, + Devnet, + Local, + Custom, +} + +impl FromStr for Network { + type Err = CliError; + + fn from_str(s: &str) -> Result { + Ok(match s.to_lowercase().trim() { + "testnet" => Self::Testnet, + "devnet" => Self::Devnet, + "local" => Self::Local, + "custom" => Self::Custom, + str => { + return Err(CliError::CommandArgumentError(format!( + "Invalid network {}. Must be one of [testnet, devnet, local, custom]", + str + ))) + } + }) + } +} + +impl Default for Network { + fn default() -> Self { + // This unfortunately has to be custom if people play with their configs + Self::Custom + } +} diff --git a/crates/aptos/src/common/types.rs b/crates/aptos/src/common/types.rs index 2eb54fd84da90..5fc87cadcc0ac 100644 --- a/crates/aptos/src/common/types.rs +++ b/crates/aptos/src/common/types.rs @@ -1,6 +1,7 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 +use crate::common::init::Network; use crate::common::utils::prompt_yes_with_override; use crate::{ common::{ @@ -186,6 +187,8 @@ pub const CONFIG_FOLDER: &str = ".aptos"; /// An individual profile #[derive(Debug, Default, Serialize, Deserialize)] pub struct ProfileConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub network: Option, /// Private key for commands. #[serde(skip_serializing_if = "Option::is_none")] pub private_key: Option, diff --git a/crates/aptos/src/test/mod.rs b/crates/aptos/src/test/mod.rs index 75b4aec09f676..fd66b750bf987 100644 --- a/crates/aptos/src/test/mod.rs +++ b/crates/aptos/src/test/mod.rs @@ -9,7 +9,7 @@ use crate::account::{ list::{ListAccount, ListQuery}, transfer::{TransferCoins, TransferSummary}, }; -use crate::common::init::InitTool; +use crate::common::init::{InitTool, Network}; use crate::common::types::{ account_address_from_public_key, AccountAddressWrapper, CliError, CliTypedResult, EncodingOptions, FaucetOptions, GasOptions, KeyType, MoveManifestAccountWrapper, @@ -505,6 +505,7 @@ impl CliTestFramework { pub async fn init(&self, private_key: &Ed25519PrivateKey) -> CliTypedResult<()> { InitTool { + network: Some(Network::Custom), rest_url: Some(self.endpoint.clone()), faucet_url: Some(self.faucet_endpoint.clone()), rng_args: RngArgs::from_seed([0; 32]),