diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8f6c852e..24752a663 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,8 @@ name: release on: workflow_dispatch: - # pull_request: - # branches: [master] + pull_request: + branches: [master] # push: #Enable when testing release infrastructure on a branch. # branches: [master] @@ -20,7 +20,7 @@ jobs: - name: Get the release version from the tag # if: env.NEAR_CLI_VERSION == '' run: | - echo "NEAR_CLI_VERSION=0.1.01" >> $GITHUB_ENV + echo "NEAR_CLI_VERSION=0.1.02" >> $GITHUB_ENV echo "version is: ${{ env.NEAR_CLI_VERSION }}" - name: Create GitHub release diff --git a/Cargo.lock b/Cargo.lock index 22ff9a7c7..87d356613 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1821,6 +1821,7 @@ dependencies = [ "strum 0.20.1", "strum_macros 0.20.1 (registry+/~https://github.com/rust-lang/crates.io-index)", "url", + "url_open", ] [[package]] @@ -3463,6 +3464,16 @@ dependencies = [ "serde", ] +[[package]] +name = "url_open" +version = "0.0.1" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "ae0b8a775e36e692c64e6a15a5cd854cc58dee7636d380b07738f6f816e3dc78" +dependencies = [ + "url", + "winapi", +] + [[package]] name = "uuid" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index c55f81d57..467851fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ dirs = "3.0" # The fix is needed for seemless clap integration: /~https://github.com/wusyong/slip10/pull/3 slip10 = { git = "/~https://github.com/frol/slip10", rev = "a3235dd0acf3a485f547cf23e2dd56838adb45f8" } url = { version = "2", features = ["serde"] } +url_open = "0.0.1" color-eyre = "0.5" diff --git a/src/commands/add_command/access_key/public_key_mode/generate_keypair/mod.rs b/src/commands/add_command/access_key/public_key_mode/generate_keypair/mod.rs index e66676552..8e59fed98 100644 --- a/src/commands/add_command/access_key/public_key_mode/generate_keypair/mod.rs +++ b/src/commands/add_command/access_key/public_key_mode/generate_keypair/mod.rs @@ -108,6 +108,7 @@ impl GenerateKeypair { format!("{}.json", &prepopulated_unsigned_transaction.signer_id).into(); let mut path = std::path::PathBuf::from(&home_dir); path.push(crate::consts::DIR_NAME_KEY_CHAIN); + std::fs::create_dir_all(&path)?; path.push(file_name); if path.exists() { return Err(color_eyre::Report::msg(format!( diff --git a/src/commands/add_command/contract_code/contract/mod.rs b/src/commands/add_command/contract_code/contract/mod.rs index 696f060db..d5fe605e8 100644 --- a/src/commands/add_command/contract_code/contract/mod.rs +++ b/src/commands/add_command/contract_code/contract/mod.rs @@ -111,10 +111,10 @@ impl ContractFile { prepopulated_unsigned_transaction: near_primitives::transaction::Transaction, network_connection_config: Option, ) -> crate::CliResult { - let mut f = std::fs::File::open(&self.file_path.clone()) - .map_err(|err| color_eyre::Report::msg(format!("Failed to open file: {:?}", err)))?; let mut code = Vec::new(); - f.read_to_end(&mut code) + std::fs::File::open(&self.file_path.clone()) + .map_err(|err| color_eyre::Report::msg(format!("Failed to open file: {:?}", err)))? + .read_to_end(&mut code) .map_err(|err| color_eyre::Report::msg(format!("Failed to read file: {:?}", err)))?; let action = near_primitives::transaction::Action::DeployContract( near_primitives::transaction::DeployContractAction { code }, diff --git a/src/commands/add_command/implicit_account/generate_keypair/mod.rs b/src/commands/add_command/implicit_account/generate_keypair/mod.rs index ea114e228..c2fedb0f4 100644 --- a/src/commands/add_command/implicit_account/generate_keypair/mod.rs +++ b/src/commands/add_command/implicit_account/generate_keypair/mod.rs @@ -82,6 +82,7 @@ impl CliGenerateKeypair { let file_name: std::path::PathBuf = format!("{}.json", &implicit_account_id).into(); let mut path = std::path::PathBuf::from(&home_dir); path.push(crate::consts::DIR_NAME_KEY_CHAIN); + std::fs::create_dir_all(&path)?; path.push(file_name); if path.exists() { return Err(color_eyre::Report::msg(format!( diff --git a/src/commands/add_command/sub_account/full_access_key/public_key_mode/generate_keypair/mod.rs b/src/commands/add_command/sub_account/full_access_key/public_key_mode/generate_keypair/mod.rs index 4e4d23166..fcd2f85ba 100644 --- a/src/commands/add_command/sub_account/full_access_key/public_key_mode/generate_keypair/mod.rs +++ b/src/commands/add_command/sub_account/full_access_key/public_key_mode/generate_keypair/mod.rs @@ -105,6 +105,7 @@ impl GenerateKeypair { format!("{}.json", &prepopulated_unsigned_transaction.receiver_id).into(); let mut path = std::path::PathBuf::from(&home_dir); path.push(crate::consts::DIR_NAME_KEY_CHAIN); + std::fs::create_dir_all(&path)?; path.push(file_name); if path.exists() { return Err(color_eyre::Report::msg(format!( diff --git a/src/commands/construct_transaction_command/sign_transaction/mod.rs b/src/commands/construct_transaction_command/sign_transaction/mod.rs index 17ad19387..b747034b3 100644 --- a/src/commands/construct_transaction_command/sign_transaction/mod.rs +++ b/src/commands/construct_transaction_command/sign_transaction/mod.rs @@ -2,8 +2,8 @@ use dialoguer::{theme::ColorfulTheme, Select}; use strum::{EnumDiscriminants, EnumIter, EnumMessage, IntoEnumIterator}; mod sign_manually; -mod sign_with_keychain; -mod sign_with_private_key; +pub mod sign_with_keychain; +pub mod sign_with_private_key; #[derive(Debug, clap::Clap)] pub enum CliSignTransaction { diff --git a/src/commands/construct_transaction_command/sign_transaction/sign_with_keychain/mod.rs b/src/commands/construct_transaction_command/sign_transaction/sign_with_keychain/mod.rs index fee1828bc..3bcf145cc 100644 --- a/src/commands/construct_transaction_command/sign_transaction/sign_with_keychain/mod.rs +++ b/src/commands/construct_transaction_command/sign_transaction/sign_with_keychain/mod.rs @@ -11,7 +11,7 @@ pub struct CliSignKeychain { #[derive(Debug)] pub struct SignKeychain { - submit: Option, + pub submit: Option, } impl From for SignKeychain { diff --git a/src/commands/login/mod.rs b/src/commands/login/mod.rs new file mode 100644 index 000000000..6777ebcf1 --- /dev/null +++ b/src/commands/login/mod.rs @@ -0,0 +1 @@ +pub mod operation_mode; diff --git a/src/commands/login/operation_mode/mod.rs b/src/commands/login/operation_mode/mod.rs new file mode 100644 index 000000000..c3fb24f4f --- /dev/null +++ b/src/commands/login/operation_mode/mod.rs @@ -0,0 +1,64 @@ +use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + +pub mod online_mode; + +/// инструмент выбора режима online/offline +#[derive(Debug, Default, clap::Clap)] +pub struct CliOperationMode { + #[clap(subcommand)] + mode: Option, +} + +#[derive(Debug)] +pub struct OperationMode { + pub mode: Mode, +} + +impl From for OperationMode { + fn from(item: CliOperationMode) -> Self { + let mode = match item.mode { + Some(cli_mode) => Mode::from(cli_mode), + None => Mode::choose_mode(), + }; + Self { mode } + } +} + +impl OperationMode { + pub async fn process(self) -> crate::CliResult { + self.mode.process().await + } +} + +#[derive(Debug, clap::Clap)] +pub enum CliMode { + /// Execute a change method with online mode + Network(self::online_mode::CliNetworkArgs), +} + +#[derive(Debug, EnumDiscriminants)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +pub enum Mode { + #[strum_discriminants(strum(message = "Yes, I keep it simple"))] + Network(self::online_mode::NetworkArgs), +} + +impl From for Mode { + fn from(item: CliMode) -> Self { + match item { + CliMode::Network(cli_network_args) => Self::Network(cli_network_args.into()), + } + } +} + +impl Mode { + pub fn choose_mode() -> Self { + Self::from(CliMode::Network(Default::default())) + } + + pub async fn process(self) -> crate::CliResult { + match self { + Self::Network(network_args) => network_args.process().await, + } + } +} diff --git a/src/commands/login/operation_mode/online_mode/mod.rs b/src/commands/login/operation_mode/online_mode/mod.rs new file mode 100644 index 000000000..3347a481b --- /dev/null +++ b/src/commands/login/operation_mode/online_mode/mod.rs @@ -0,0 +1,31 @@ +pub mod select_server; + +/// аргументы, необходимые для создания транзакции в online mode +#[derive(Debug, Default, clap::Clap)] +pub struct CliNetworkArgs { + #[clap(subcommand)] + selected_server: Option, +} + +#[derive(Debug)] +pub struct NetworkArgs { + selected_server: self::select_server::SelectServer, +} + +impl From for NetworkArgs { + fn from(item: CliNetworkArgs) -> Self { + let selected_server = match item.selected_server { + Some(cli_selected_server) => { + self::select_server::SelectServer::from(cli_selected_server) + } + None => self::select_server::SelectServer::choose_server(), + }; + Self { selected_server } + } +} + +impl NetworkArgs { + pub async fn process(self) -> crate::CliResult { + self.selected_server.process().await + } +} diff --git a/src/commands/login/operation_mode/online_mode/select_server/mod.rs b/src/commands/login/operation_mode/online_mode/select_server/mod.rs new file mode 100644 index 000000000..3365d1902 --- /dev/null +++ b/src/commands/login/operation_mode/online_mode/select_server/mod.rs @@ -0,0 +1,89 @@ +use dialoguer::{theme::ColorfulTheme, Select}; +use strum::{EnumDiscriminants, EnumIter, EnumMessage, IntoEnumIterator}; + +pub mod server; + +#[derive(Debug, clap::Clap)] +pub enum CliSelectServer { + /// предоставление данных для сервера https://rpc.testnet.near.org + Testnet(self::server::CliServer), + /// предоставление данных для сервера https://rpc.mainnet.near.org + Mainnet(self::server::CliServer), + /// предоставление данных для сервера https://rpc.betanet.near.org + Betanet(self::server::CliServer), + /// предоставление данных для сервера, указанного вручную + Custom(self::server::CliCustomServer), +} + +#[derive(Debug, EnumDiscriminants)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +pub enum SelectServer { + #[strum_discriminants(strum(message = "Testnet"))] + Testnet(self::server::Server), + #[strum_discriminants(strum(message = "Mainnet"))] + Mainnet(self::server::Server), + #[strum_discriminants(strum(message = "Betanet"))] + Betanet(self::server::Server), + #[strum_discriminants(strum(message = "Custom"))] + Custom(self::server::Server), +} + +impl From for SelectServer { + fn from(item: CliSelectServer) -> Self { + match item { + CliSelectServer::Testnet(cli_server) => { + Self::Testnet(cli_server.into_server(crate::common::ConnectionConfig::Testnet)) + } + CliSelectServer::Mainnet(cli_server) => { + Self::Mainnet(cli_server.into_server(crate::common::ConnectionConfig::Mainnet)) + } + CliSelectServer::Betanet(cli_server) => { + Self::Betanet(cli_server.into_server(crate::common::ConnectionConfig::Betanet)) + } + CliSelectServer::Custom(cli_custom_server) => { + Self::Custom(cli_custom_server.into_server()) + } + } + } +} + +impl SelectServer { + pub fn choose_server() -> Self { + println!(); + let variants = SelectServerDiscriminants::iter().collect::>(); + let servers = variants + .iter() + .map(|p| p.get_message().unwrap().to_owned()) + .collect::>(); + let selected_server = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Select NEAR protocol wallet url") + .items(&servers) + .default(0) + .interact() + .unwrap(); + let cli_select_server = match variants[selected_server] { + SelectServerDiscriminants::Testnet => CliSelectServer::Testnet(Default::default()), + SelectServerDiscriminants::Mainnet => CliSelectServer::Mainnet(Default::default()), + SelectServerDiscriminants::Betanet => CliSelectServer::Betanet(Default::default()), + SelectServerDiscriminants::Custom => CliSelectServer::Custom(Default::default()), + }; + Self::from(cli_select_server) + } + + pub async fn process(self) -> crate::CliResult { + Ok(match self { + SelectServer::Testnet(server) => { + server.process().await?; + } + SelectServer::Mainnet(server) => { + server.process().await?; + } + SelectServer::Betanet(server) => { + server.process().await?; + } + SelectServer::Custom(server) => { + server.process().await?; + } + }) + } +} diff --git a/src/commands/login/operation_mode/online_mode/select_server/server/mod.rs b/src/commands/login/operation_mode/online_mode/select_server/server/mod.rs new file mode 100644 index 000000000..2f26bb87b --- /dev/null +++ b/src/commands/login/operation_mode/online_mode/select_server/server/mod.rs @@ -0,0 +1,157 @@ +use std::io::Write; +use std::str::FromStr; + +use dialoguer::Input; +use url_open::UrlOpen; + +/// предустановленный RPC-сервер +#[derive(Debug, Default, clap::Clap)] +pub struct CliServer {} + +/// данные для custom server +#[derive(Debug, Default, clap::Clap)] +pub struct CliCustomServer { + #[clap(long)] + pub url: Option, +} + +#[derive(Debug)] +pub struct Server { + pub connection_config: crate::common::ConnectionConfig, +} + +impl CliServer { + pub fn into_server(self, connection_config: crate::common::ConnectionConfig) -> Server { + Server { connection_config } + } +} + +impl CliCustomServer { + pub fn into_server(self) -> Server { + let url: url::Url = match self.url { + Some(url) => url, + None => Input::new() + .with_prompt("What is the wallet url?") + .interact_text() + .unwrap(), + }; + Server { + connection_config: crate::common::ConnectionConfig::Custom { url }, + } + } +} + +impl Server { + pub async fn process(self) -> crate::CliResult { + let generate_keypair: crate::commands::utils_command::generate_keypair_subcommand::CliGenerateKeypair = + crate::commands::utils_command::generate_keypair_subcommand::CliGenerateKeypair::default(); + + let key_pair_properties: crate::common::KeyPairProperties = + crate::common::generate_keypair( + generate_keypair.master_seed_phrase.as_deref(), + generate_keypair.new_master_seed_phrase_words_count, + generate_keypair.seed_phrase_hd_path, + ) + .await?; + + let mut url: url::Url = self.connection_config.wallet_url().join("login/")?; + url.query_pairs_mut() + .append_pair("title", "NEAR CLI") + .append_pair("public_key", &key_pair_properties.public_key_str); + // Use `success_url` once capture mode is implemented + //.append_pair("success_url", "http://127.0.0.1:8080"); + println!( + "If your browser doesn't automatically open, please visit this URL:\n {}\n", + &url.as_str() + ); + url.open(); + + let public_key: near_crypto::PublicKey = + near_crypto::PublicKey::from_str(&key_pair_properties.public_key_str)?; + + let account_id = get_account_from_cli(public_key, self.connection_config.clone()).await?; + if !account_id.is_empty() { + save_account(&account_id, key_pair_properties, self.connection_config).await? + }; + Ok(()) + } +} + +async fn get_account_from_cli( + public_key: near_crypto::PublicKey, + network_connection_config: crate::common::ConnectionConfig, +) -> color_eyre::eyre::Result { + let account_id = input_account_id(); + verify_account_id(account_id.clone(), public_key, network_connection_config) + .await + .map_err(|err| color_eyre::Report::msg(format!("Failed account ID: {:?}", err)))?; + Ok(account_id) +} + +fn input_account_id() -> String { + Input::new() + .with_prompt("Enter account ID") + .interact_text() + .unwrap() +} + +fn rpc_client(selected_server_url: &str) -> near_jsonrpc_client::JsonRpcClient { + near_jsonrpc_client::new_client(&selected_server_url) +} + +async fn verify_account_id( + account_id: String, + public_key: near_crypto::PublicKey, + network_connection_config: crate::common::ConnectionConfig, +) -> crate::CliResult { + rpc_client(network_connection_config.rpc_url().as_str()) + .query(near_jsonrpc_primitives::types::query::RpcQueryRequest { + block_reference: near_primitives::types::Finality::Final.into(), + request: near_primitives::views::QueryRequest::ViewAccessKey { + account_id, + public_key, + }, + }) + .await + .map_err(|err| { + color_eyre::Report::msg(format!( + "Failed to fetch query for view access key: {:?}", + err + )) + })?; + Ok(()) +} + +async fn save_account( + account_id: &str, + key_pair_properties: crate::common::KeyPairProperties, + network_connection_config: crate::common::ConnectionConfig, +) -> crate::CliResult { + let buf = format!( + "{}", + serde_json::json!({ + "account_id": account_id, + "public_key": key_pair_properties.public_key_str.clone(), + "private_key": key_pair_properties.secret_keypair_str.clone(), + }) + ); + let home_dir = dirs::home_dir().expect("Impossible to get your home dir!"); + let file_name: std::path::PathBuf = format!("{}.json", &account_id).into(); + let mut path = std::path::PathBuf::from(&home_dir); + path.push(network_connection_config.dir_name()); + std::fs::create_dir_all(&path)?; + path.push(file_name); + std::fs::File::create(&path) + .map_err(|err| color_eyre::Report::msg(format!("Failed to create file: {:?}", err)))? + .write(buf.as_bytes()) + .map_err(|err| color_eyre::Report::msg(format!("Failed to write to file: {:?}", err)))?; + println!( + "\n\n\nThe data for the access key is saved in a file {}", + &path.display() + ); + println!( + "Logged in as [ {} ] with public key [ {} ] successfully", + account_id, key_pair_properties.public_key_str + ); + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 47ba381d2..edc6f0d84 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod construct_transaction_command; pub mod delete_command; pub mod execute_command; pub mod generate_shell_completions_command; +pub mod login; pub mod transfer_command; pub mod utils_command; pub mod view_command; @@ -22,6 +23,8 @@ pub enum CliTopLevelCommand { Execute(self::execute_command::CliOptionMethod), /// Use these to generate static shell completions GenerateShellCompletions(self::generate_shell_completions_command::CliGenerateShellCompletions), + /// Use these to login with wallet authorization + Login(self::login::operation_mode::CliOperationMode), /// Use these to transfer tokens Transfer(self::transfer_command::CliCurrency), /// Helpers @@ -33,6 +36,8 @@ pub enum CliTopLevelCommand { #[derive(Debug, EnumDiscriminants)] #[strum_discriminants(derive(EnumMessage, EnumIter))] pub enum TopLevelCommand { + #[strum_discriminants(strum(message = "Login with wallet authorization"))] + Login(self::login::operation_mode::OperationMode), #[strum_discriminants(strum( message = "View account, contract code, contract state, transaction, nonce" ))] @@ -69,6 +74,9 @@ impl From for TopLevelCommand { CliTopLevelCommand::GenerateShellCompletions(_) => { unreachable!("This variant is handled in the main function") } + CliTopLevelCommand::Login(cli_option_method) => { + TopLevelCommand::Login(cli_option_method.into()) + } CliTopLevelCommand::Transfer(cli_currency) => { TopLevelCommand::Transfer(cli_currency.into()) } @@ -103,6 +111,7 @@ impl TopLevelCommand { TopLevelCommandDiscriminants::Execute => { CliTopLevelCommand::Execute(Default::default()) } + TopLevelCommandDiscriminants::Login => CliTopLevelCommand::Login(Default::default()), TopLevelCommandDiscriminants::Transfer => { CliTopLevelCommand::Transfer(Default::default()) } @@ -126,6 +135,7 @@ impl TopLevelCommand { Self::ConstructTransaction(mode) => mode.process(unsigned_transaction).await, Self::Delete(delete_action) => delete_action.process(unsigned_transaction).await, Self::Execute(option_method) => option_method.process(unsigned_transaction).await, + Self::Login(mode) => mode.process().await, Self::Transfer(currency) => currency.process(unsigned_transaction).await, Self::Utils(util_type) => util_type.process().await, Self::View(view_query_request) => view_query_request.process().await, diff --git a/src/commands/utils_command/generate_keypair_subcommand/mod.rs b/src/commands/utils_command/generate_keypair_subcommand/mod.rs index a66338177..6ba35d2a7 100644 --- a/src/commands/utils_command/generate_keypair_subcommand/mod.rs +++ b/src/commands/utils_command/generate_keypair_subcommand/mod.rs @@ -21,7 +21,7 @@ fn bip32path_to_string(bip32path: &slip10::BIP32Path) -> String { /// Generate a key pair of secret and public keys (use it anywhere you need /// Ed25519 keys) -#[derive(Debug, clap::Clap)] +#[derive(Debug, clap::Clap, Clone)] pub struct CliGenerateKeypair { pub master_seed_phrase: Option, pub new_master_seed_phrase_words_count: usize, @@ -42,73 +42,34 @@ impl Default for CliGenerateKeypair { impl CliGenerateKeypair { pub async fn process(self) -> crate::CliResult { - let (master_seed_phrase, master_seed) = - if let Some(ref master_seed_phrase) = self.master_seed_phrase { - ( - master_seed_phrase.clone(), - bip39::Mnemonic::parse(master_seed_phrase)?.to_seed(""), - ) - } else { - let mnemonic = bip39::Mnemonic::generate(self.new_master_seed_phrase_words_count)?; - let mut master_seed_phrase = String::new(); - for (index, word) in mnemonic.word_iter().enumerate() { - if index != 0 { - master_seed_phrase.push(' '); - } - master_seed_phrase.push_str(word); - } - (master_seed_phrase, mnemonic.to_seed("")) - }; - - let derived_private_key = slip10::derive_key_from_path( - &master_seed, - slip10::Curve::Ed25519, - &self.seed_phrase_hd_path, + let key_pair_properties = crate::common::generate_keypair( + self.master_seed_phrase.as_deref(), + self.new_master_seed_phrase_words_count, + self.seed_phrase_hd_path, ) - .map_err(|err| { - color_eyre::Report::msg(format!( - "Failed to derive a key from the master key: {}", - err - )) - })?; - - let secret_keypair = { - let secret = ed25519_dalek::SecretKey::from_bytes(&derived_private_key.key)?; - let public = ed25519_dalek::PublicKey::from(&secret); - ed25519_dalek::Keypair { secret, public } - }; - - let implicit_account_id = hex::encode(&secret_keypair.public); - let public_key_str = format!( - "ed25519:{}", - bs58::encode(&secret_keypair.public).into_string() - ); - let secret_keypair_str = format!( - "ed25519:{}", - bs58::encode(secret_keypair.to_bytes()).into_string() - ); + .await?; match self.format { crate::common::OutputFormat::Plaintext => { println!( "Master Seed Phrase: {}\nSeed Phrase HD Path: {}\nImplicit Account ID: {}\nPublic Key: {}\nSECRET KEYPAIR: {}", - master_seed_phrase, - bip32path_to_string(&self.seed_phrase_hd_path), - implicit_account_id, - public_key_str, - secret_keypair_str, + key_pair_properties.master_seed_phrase, + bip32path_to_string(&key_pair_properties.seed_phrase_hd_path), + key_pair_properties.implicit_account_id, + key_pair_properties.public_key_str, + key_pair_properties.secret_keypair_str, ); } crate::common::OutputFormat::Json => { println!( - "{:#?}", - serde_json::json!({ - "master_seed_phrase": master_seed_phrase, - "seed_phrase_hd_path": bip32path_to_string(&self.seed_phrase_hd_path), - "account_id": implicit_account_id, - "public_key": public_key_str, - "private_key": secret_keypair_str, - }) + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "master_seed_phrase": key_pair_properties.master_seed_phrase, + "seed_phrase_hd_path": bip32path_to_string(&key_pair_properties.seed_phrase_hd_path), + "account_id": key_pair_properties.implicit_account_id, + "public_key": key_pair_properties.public_key_str, + "private_key": key_pair_properties.secret_keypair_str, + })).unwrap() ); } }; diff --git a/src/commands/utils_command/mod.rs b/src/commands/utils_command/mod.rs index 049ddb006..0c7c1049c 100644 --- a/src/commands/utils_command/mod.rs +++ b/src/commands/utils_command/mod.rs @@ -2,7 +2,7 @@ use dialoguer::{theme::ColorfulTheme, Select}; use strum::{EnumDiscriminants, EnumIter, EnumMessage, IntoEnumIterator}; mod combine_transaction_subcommand_with_signature; -mod generate_keypair_subcommand; +pub mod generate_keypair_subcommand; mod sign_transaction_subcommand_with_secret_key; /// набор утилит-помощников diff --git a/src/commands/view_command/view_contract_code/block_id/block_id_hash/mod.rs b/src/commands/view_command/view_contract_code/block_id/block_id_hash/mod.rs index 3fce35526..fcd6cbb29 100644 --- a/src/commands/view_command/view_contract_code/block_id/block_id_hash/mod.rs +++ b/src/commands/view_command/view_contract_code/block_id/block_id_hash/mod.rs @@ -67,6 +67,8 @@ impl BlockIdHash { }; match &file_path { Some(file_path) => { + let dir_name = &file_path.parent().unwrap(); + std::fs::create_dir_all(&dir_name)?; std::fs::File::create(file_path) .map_err(|err| { color_eyre::Report::msg(format!("Failed to create file: {:?}", err)) diff --git a/src/commands/view_command/view_contract_code/block_id/block_id_height/mod.rs b/src/commands/view_command/view_contract_code/block_id/block_id_height/mod.rs index fbe60d53b..9137b61b5 100644 --- a/src/commands/view_command/view_contract_code/block_id/block_id_height/mod.rs +++ b/src/commands/view_command/view_contract_code/block_id/block_id_height/mod.rs @@ -67,6 +67,8 @@ impl BlockIdHeight { }; match &file_path { Some(file_path) => { + let dir_name = &file_path.parent().unwrap(); + std::fs::create_dir_all(&dir_name)?; std::fs::File::create(file_path) .map_err(|err| { color_eyre::Report::msg(format!("Failed to create file: {:?}", err)) diff --git a/src/commands/view_command/view_contract_code/block_id/mod.rs b/src/commands/view_command/view_contract_code/block_id/mod.rs index d4edfd719..b7c773107 100644 --- a/src/commands/view_command/view_contract_code/block_id/mod.rs +++ b/src/commands/view_command/view_contract_code/block_id/mod.rs @@ -122,6 +122,8 @@ impl BlockId { }; match &file_path { Some(file_path) => { + let dir_name = &file_path.parent().unwrap(); + std::fs::create_dir_all(&dir_name)?; std::fs::File::create(file_path) .map_err(|err| { color_eyre::Report::msg(format!("Failed to create file: {:?}", err)) diff --git a/src/common.rs b/src/common.rs index f6df9ad44..0435f4e27 100644 --- a/src/common.rs +++ b/src/common.rs @@ -4,6 +4,7 @@ use near_primitives::borsh::BorshDeserialize; #[derive( Debug, + Clone, strum_macros::IntoStaticStr, strum_macros::EnumString, strum_macros::EnumVariantNames, @@ -236,7 +237,7 @@ impl NearGas { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ConnectionConfig { Testnet, Mainnet, @@ -268,6 +269,83 @@ impl ConnectionConfig { Self::Custom { url } => url.clone(), } } + + pub fn wallet_url(&self) -> url::Url { + match self { + Self::Testnet => crate::consts::TESTNET_WALLET_URL.parse().unwrap(), + Self::Mainnet => crate::consts::MAINNET_WALLET_URL.parse().unwrap(), + Self::Betanet => crate::consts::BETANET_WALLET_URL.parse().unwrap(), + Self::Custom { url } => url.clone(), + } + } + + pub fn dir_name(&self) -> &str { + match self { + Self::Testnet => crate::consts::DIR_NAME_TESTNET, + Self::Mainnet => crate::consts::DIR_NAME_MAINNET, + Self::Betanet => crate::consts::DIR_NAME_BETANET, + Self::Custom { url: _ } => crate::consts::DIR_NAME_CUSTOM, + } + } +} + +#[derive(Debug)] +pub struct KeyPairProperties { + pub seed_phrase_hd_path: slip10::BIP32Path, + pub master_seed_phrase: String, + pub implicit_account_id: String, + pub public_key_str: String, + pub secret_keypair_str: String, +} + +pub async fn generate_keypair( + master_seed_phrase: Option<&str>, + new_master_seed_phrase_words_count: usize, + seed_phrase_hd_path: slip10::BIP32Path, +) -> color_eyre::eyre::Result { + let (master_seed_phrase, master_seed) = if let Some(master_seed_phrase) = master_seed_phrase { + ( + master_seed_phrase.to_owned(), + bip39::Mnemonic::parse(master_seed_phrase)?.to_seed(""), + ) + } else { + let mnemonic = bip39::Mnemonic::generate(new_master_seed_phrase_words_count)?; + let master_seed_phrase = mnemonic.word_iter().collect::>().join(" "); + (master_seed_phrase, mnemonic.to_seed("")) + }; + + let derived_private_key = + slip10::derive_key_from_path(&master_seed, slip10::Curve::Ed25519, &seed_phrase_hd_path) + .map_err(|err| { + color_eyre::Report::msg(format!( + "Failed to derive a key from the master key: {}", + err + )) + })?; + + let secret_keypair = { + let secret = ed25519_dalek::SecretKey::from_bytes(&derived_private_key.key)?; + let public = ed25519_dalek::PublicKey::from(&secret); + ed25519_dalek::Keypair { secret, public } + }; + + let implicit_account_id = hex::encode(&secret_keypair.public); + let public_key_str = format!( + "ed25519:{}", + bs58::encode(&secret_keypair.public).into_string() + ); + let secret_keypair_str = format!( + "ed25519:{}", + bs58::encode(secret_keypair.to_bytes()).into_string() + ); + let key_pair_properties: KeyPairProperties = KeyPairProperties { + seed_phrase_hd_path, + master_seed_phrase, + implicit_account_id, + public_key_str, + secret_keypair_str, + }; + Ok(key_pair_properties) } #[cfg(test)] diff --git a/src/consts.rs b/src/consts.rs index b430c5535..1cb5b8b08 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -6,4 +6,12 @@ pub const BETANET_API_SERVER_URL: &str = "https://rpc.betanet.near.org"; // NOTE: There is no dedicated archival RPC server for betanet by design pub const BETANET_ARCHIVAL_API_SERVER_URL: &str = "https://rpc.betanet.near.org"; +pub const TESTNET_WALLET_URL: &str = "https://wallet.testnet.near.org"; +pub const MAINNET_WALLET_URL: &str = "https://wallet.mainnet.near.org"; +pub const BETANET_WALLET_URL: &str = "https://wallet.betanet.near.org"; + pub const DIR_NAME_KEY_CHAIN: &str = ".near-credentials/default/"; +pub const DIR_NAME_TESTNET: &str = ".near-credentials/testnet/"; +pub const DIR_NAME_MAINNET: &str = ".near-credentials/mainnet/"; +pub const DIR_NAME_BETANET: &str = ".near-credentials/betanet/"; +pub const DIR_NAME_CUSTOM: &str = ".near-credentials/default/";