diff --git a/src/commands/account/create_account/create_implicit_account/use_seed_phrase.rs b/src/commands/account/create_account/create_implicit_account/use_seed_phrase.rs index 2c69d0fb..29a270e1 100644 --- a/src/commands/account/create_account/create_implicit_account/use_seed_phrase.rs +++ b/src/commands/account/create_account/create_implicit_account/use_seed_phrase.rs @@ -1,7 +1,6 @@ use std::io::Write; use color_eyre::eyre::Context; -use inquire::CustomType; #[derive(Debug, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(input_context = crate::GlobalContext)] @@ -74,10 +73,6 @@ impl SaveWithSeedPhrase { pub fn input_seed_phrase_hd_path( _context: &crate::GlobalContext, ) -> color_eyre::eyre::Result> { - Ok(Some( - CustomType::new("Enter seed phrase HD Path (if you not sure leave blank for default):") - .with_starting_input("m/44'/397'/0'") - .prompt()?, - )) + crate::transaction_signature_options::sign_with_seed_phrase::input_seed_phrase_hd_path() } } diff --git a/src/commands/account/get_public_key/from_keychain/mod.rs b/src/commands/account/get_public_key/from_keychain/mod.rs new file mode 100644 index 00000000..75ed6aa0 --- /dev/null +++ b/src/commands/account/get_public_key/from_keychain/mod.rs @@ -0,0 +1,114 @@ +use color_eyre::eyre::WrapErr; + +use crate::common::JsonRpcClientExt; +use crate::common::RpcQueryResponseExt; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = PublicKeyFromKeychainContext)] +pub struct PublicKeyFromKeychain { + #[interactive_clap(skip_default_input_arg)] + /// For which account do you need to view the public key? + owner_account_id: crate::types::account_id::AccountId, + #[interactive_clap(named_arg)] + /// Select network + network_config: crate::network::Network, +} + +#[derive(Clone)] +pub struct PublicKeyFromKeychainContext(crate::network::NetworkContext); + +impl PublicKeyFromKeychainContext { + pub fn from_previous_context( + previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let account_id = scope.owner_account_id.clone(); + + let on_after_getting_network_callback: crate::network::OnAfterGettingNetworkCallback = + std::sync::Arc::new({ + move |network_config| { + if previous_context.offline { + eprintln!( + "\nThe signer's public key cannot be verified and retrieved offline." + ); + return Ok(()); + } + let service_name = std::borrow::Cow::Owned(format!( + "near-{}-{}", + network_config.network_name, &account_id + )); + + let password = { + let access_key_list = network_config + .json_rpc_client() + .blocking_call_view_access_key_list( + &account_id.clone().into(), + near_primitives::types::Finality::Final.into(), + ) + .wrap_err_with(|| { + format!("Failed to fetch access key list for {}", account_id) + })? + .access_key_list_view()?; + + let res = access_key_list + .keys + .into_iter() + .filter(|key| { + matches!( + key.access_key.permission, + near_primitives::views::AccessKeyPermissionView::FullAccess + ) + }) + .map(|key| key.public_key) + .find_map(|public_key| { + let keyring = keyring::Entry::new( + &service_name, + &format!("{}:{}", account_id, public_key), + ) + .ok()?; + keyring.get_password().ok() + }); + + match res { + Some(password) => password, + None => { + // no access keys found + eprintln!("\nNo access keys found in keychain",); + return Ok(()); + } + } + }; + + let account_key_pair: crate::transaction_signature_options::AccountKeyPair = + serde_json::from_str(&password).wrap_err("Error reading data")?; + eprintln!("\nPublic key: {}", account_key_pair.public_key); + + Ok(()) + } + }); + + Ok(Self(crate::network::NetworkContext { + config: previous_context.config, + interacting_with_account_ids: vec![scope.owner_account_id.clone().into()], + on_after_getting_network_callback, + })) + } +} + +impl PublicKeyFromKeychain { + pub fn input_owner_account_id( + context: &crate::GlobalContext, + ) -> color_eyre::eyre::Result> { + crate::common::input_signer_account_id_from_used_account_list( + &context.config.credentials_home_dir, + "For which account do you need to view the public key?", + ) + } +} + +impl From for crate::network::NetworkContext { + fn from(item: PublicKeyFromKeychainContext) -> Self { + item.0 + } +} diff --git a/src/commands/account/get_public_key/from_ledger/mod.rs b/src/commands/account/get_public_key/from_ledger/mod.rs new file mode 100644 index 00000000..4667468a --- /dev/null +++ b/src/commands/account/get_public_key/from_ledger/mod.rs @@ -0,0 +1,55 @@ +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = PublicKeyFromLedgerContext)] +pub struct PublicKeyFromLedger { + #[interactive_clap(long)] + #[interactive_clap(skip_default_input_arg)] + seed_phrase_hd_path: crate::types::slip10::BIP32Path, +} + +#[derive(Debug, Clone)] +pub struct PublicKeyFromLedgerContext {} + +impl PublicKeyFromLedgerContext { + pub fn from_previous_context( + _previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let seed_phrase_hd_path = scope.seed_phrase_hd_path.clone(); + eprintln!("Opening the NEAR application... Please approve opening the application"); + near_ledger::open_near_application().map_err(|ledger_error| { + color_eyre::Report::msg(format!("An error happened while trying to open the NEAR application on the ledger: {ledger_error:?}")) + })?; + + std::thread::sleep(std::time::Duration::from_secs(1)); + + eprintln!( + "Please allow getting the PublicKey on Ledger device (HD Path: {})", + seed_phrase_hd_path + ); + let public_key = near_ledger::get_public_key(seed_phrase_hd_path.into()).map_err( + |near_ledger_error| { + color_eyre::Report::msg(format!( + "An error occurred while trying to get PublicKey from Ledger device: {:?}", + near_ledger_error + )) + }, + )?; + eprintln!( + "\nPublic key: {}", + near_crypto::PublicKey::ED25519(near_crypto::ED25519PublicKey::from( + public_key.to_bytes(), + )) + ); + + Ok(Self {}) + } +} + +impl PublicKeyFromLedger { + pub fn input_seed_phrase_hd_path( + _context: &crate::GlobalContext, + ) -> color_eyre::eyre::Result> { + crate::transaction_signature_options::sign_with_ledger::input_seed_phrase_hd_path() + } +} diff --git a/src/commands/account/get_public_key/from_legacy_keychain/mod.rs b/src/commands/account/get_public_key/from_legacy_keychain/mod.rs new file mode 100644 index 00000000..60679892 --- /dev/null +++ b/src/commands/account/get_public_key/from_legacy_keychain/mod.rs @@ -0,0 +1,133 @@ +use color_eyre::eyre::WrapErr; + +use crate::common::JsonRpcClientExt; +use crate::common::RpcQueryResponseExt; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = PublicKeyFromLegacyKeychainContext)] +pub struct PublicKeyFromKeychain { + #[interactive_clap(skip_default_input_arg)] + /// For which account do you need to view the public key? + owner_account_id: crate::types::account_id::AccountId, + #[interactive_clap(named_arg)] + /// Select network + network_config: crate::network::Network, +} + +#[derive(Clone)] +pub struct PublicKeyFromLegacyKeychainContext(crate::network::NetworkContext); + +impl PublicKeyFromLegacyKeychainContext { + pub fn from_previous_context( + previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let config = previous_context.config.clone(); + let account_id = scope.owner_account_id.clone(); + + let on_after_getting_network_callback: crate::network::OnAfterGettingNetworkCallback = + std::sync::Arc::new({ + move |network_config| { + let keychain_folder = config + .credentials_home_dir + .join(&network_config.network_name); + let signer_keychain_folder = keychain_folder.join(account_id.to_string()); + let signer_access_key_file_path: std::path::PathBuf = { + if previous_context.offline { + eprintln!( + "\nThe signer's public key cannot be verified and retrieved offline." + ); + return Ok(()); + } + if signer_keychain_folder.exists() { + let full_access_key_filenames = network_config + .json_rpc_client() + .blocking_call_view_access_key_list( + &account_id.clone().into(), + near_primitives::types::Finality::Final.into(), + ) + .wrap_err_with(|| { + format!( + "Failed to fetch access KeyList for {}", + account_id + ) + })? + .access_key_list_view()? + .keys + .iter() + .filter( + |access_key_info| match access_key_info.access_key.permission { + near_primitives::views::AccessKeyPermissionView::FullAccess => true, + near_primitives::views::AccessKeyPermissionView::FunctionCall { + .. + } => false, + }, + ) + .map(|access_key_info| { + format!( + "{}.json", + access_key_info.public_key.to_string().replace(":", "_") + ) + .into() + }) + .collect::>(); + + signer_keychain_folder + .read_dir() + .wrap_err("There are no access keys found in the keychain for the signer account. Import an access key for an account before signing transactions with keychain.")? + .filter_map(Result::ok) + .find(|entry| full_access_key_filenames.contains(&entry.file_name())) + .map(|signer_access_key| signer_access_key.path()) + .unwrap_or_else(|| keychain_folder.join(format!( + "{}.json", + account_id + ))) + } else { + keychain_folder.join(format!("{}.json", account_id)) + } + }; + let signer_access_key_json = + std::fs::read(&signer_access_key_file_path).wrap_err_with(|| { + format!( + "Access key file for account <{}> on network <{}> not found! \nSearch location: {:?}", + account_id, + network_config.network_name, signer_access_key_file_path + ) + })?; + let account_key_pair: crate::transaction_signature_options::AccountKeyPair = + serde_json::from_slice(&signer_access_key_json).wrap_err_with(|| { + format!( + "Error reading data from file: {:?}", + &signer_access_key_file_path + ) + })?; + eprintln!("\nPublic key: {}", account_key_pair.public_key); + Ok(()) + } + }); + + Ok(Self(crate::network::NetworkContext { + config: previous_context.config, + interacting_with_account_ids: vec![scope.owner_account_id.clone().into()], + on_after_getting_network_callback, + })) + } +} + +impl PublicKeyFromKeychain { + pub fn input_owner_account_id( + context: &crate::GlobalContext, + ) -> color_eyre::eyre::Result> { + crate::common::input_signer_account_id_from_used_account_list( + &context.config.credentials_home_dir, + "For which account do you need to view the public key?", + ) + } +} + +impl From for crate::network::NetworkContext { + fn from(item: PublicKeyFromLegacyKeychainContext) -> Self { + item.0 + } +} diff --git a/src/commands/account/get_public_key/from_seed_phrase/mod.rs b/src/commands/account/get_public_key/from_seed_phrase/mod.rs new file mode 100644 index 00000000..5549d771 --- /dev/null +++ b/src/commands/account/get_public_key/from_seed_phrase/mod.rs @@ -0,0 +1,36 @@ +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = PublicKeyFromSeedPhraseContext)] +pub struct PublicKeyFromSeedPhrase { + /// Enter the seed-phrase: + master_seed_phrase: String, + #[interactive_clap(long)] + #[interactive_clap(skip_default_input_arg)] + seed_phrase_hd_path: crate::types::slip10::BIP32Path, +} + +#[derive(Debug, Clone)] +pub struct PublicKeyFromSeedPhraseContext; + +impl PublicKeyFromSeedPhraseContext { + pub fn from_previous_context( + _previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let public_key = crate::common::get_public_key_from_seed_phrase( + scope.seed_phrase_hd_path.clone().into(), + &scope.master_seed_phrase, + )?; + eprintln!("\nPublic key: {}", public_key); + + Ok(Self) + } +} + +impl PublicKeyFromSeedPhrase { + pub fn input_seed_phrase_hd_path( + _context: &crate::GlobalContext, + ) -> color_eyre::eyre::Result> { + crate::transaction_signature_options::sign_with_seed_phrase::input_seed_phrase_hd_path() + } +} diff --git a/src/commands/account/get_public_key/mod.rs b/src/commands/account/get_public_key/mod.rs new file mode 100644 index 00000000..7036313d --- /dev/null +++ b/src/commands/account/get_public_key/mod.rs @@ -0,0 +1,42 @@ +use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + +mod from_keychain; +#[cfg(feature = "ledger")] +mod from_ledger; +mod from_legacy_keychain; +mod from_seed_phrase; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = crate::GlobalContext)] +pub struct GetPublicKey { + #[interactive_clap(subcommand)] + get_public_key_mode: GetPublicKeyMode, +} + +#[derive(Debug, Clone, EnumDiscriminants, interactive_clap::InteractiveClap)] +#[interactive_clap(context = crate::GlobalContext)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +/// Where do you want to get the public key from? +pub enum GetPublicKeyMode { + #[cfg(feature = "ledger")] + #[strum_discriminants(strum( + message = "from-ledger - Get the public key stored on your Ledger Nano device" + ))] + /// Get the public key stored on your Ledger Nano device + FromLedger(self::from_ledger::PublicKeyFromLedger), + #[strum_discriminants(strum( + message = "from-seed-phrase - Get the public key with the seed phrase" + ))] + /// Get the public key with the seed phrase + FromSeedPhrase(self::from_seed_phrase::PublicKeyFromSeedPhrase), + #[strum_discriminants(strum( + message = "from-keychain - Get the public key stored in a secure keychain" + ))] + /// Get the public key (full access key) stored in a secure keychain + FromKeychain(self::from_keychain::PublicKeyFromKeychain), + #[strum_discriminants(strum( + message = "from-legacy-keychain - Get the public key stored in the legacy keychain (compatible with the old near CLI)" + ))] + /// Get the public key (full access key) stored in the legacy keychain (compatible with the old near CLI) + FromLegacyKeychain(self::from_legacy_keychain::PublicKeyFromKeychain), +} diff --git a/src/commands/account/import_account/using_seed_phrase/mod.rs b/src/commands/account/import_account/using_seed_phrase/mod.rs index e9be3568..b1518c82 100644 --- a/src/commands/account/import_account/using_seed_phrase/mod.rs +++ b/src/commands/account/import_account/using_seed_phrase/mod.rs @@ -1,5 +1,3 @@ -use inquire::CustomType; - #[derive(Debug, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(input_context = crate::GlobalContext)] #[interactive_clap(output_context = LoginFromSeedPhraseContext)] @@ -63,10 +61,6 @@ impl LoginFromSeedPhrase { pub fn input_seed_phrase_hd_path( _context: &crate::GlobalContext, ) -> color_eyre::eyre::Result> { - Ok(Some( - CustomType::new("Enter seed phrase HD Path (if you not sure leave blank for default):") - .with_starting_input("m/44'/397'/0'") - .prompt()?, - )) + crate::transaction_signature_options::sign_with_seed_phrase::input_seed_phrase_hd_path() } } diff --git a/src/commands/account/mod.rs b/src/commands/account/mod.rs index 48aef392..da0293f2 100644 --- a/src/commands/account/mod.rs +++ b/src/commands/account/mod.rs @@ -5,6 +5,7 @@ pub mod create_account; mod delete_account; mod delete_key; mod export_account; +mod get_public_key; mod import_account; mod list_keys; pub mod storage_management; @@ -55,6 +56,11 @@ pub enum AccountActions { ))] /// View a list of access keys of an account ListKeys(self::list_keys::ViewListKeys), + #[strum_discriminants(strum( + message = "get-public-key - Get the public key to your account" + ))] + /// Get the public key to your account + GetPublicKey(self::get_public_key::GetPublicKey), #[strum_discriminants(strum( message = "add-key - Add an access key to an account" ))] diff --git a/src/common.rs b/src/common.rs index 8f817ed3..1d3636e2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2065,7 +2065,7 @@ pub fn input_network_name( network_config .linkdrop_account_id .as_ref() - .map_or(false, |linkdrop_account_id| { + .is_some_and(|linkdrop_account_id| { account_ids.iter().any(|account_id| { account_id.as_str().ends_with(linkdrop_account_id.as_str()) }) diff --git a/src/transaction_signature_options/sign_with_seed_phrase/mod.rs b/src/transaction_signature_options/sign_with_seed_phrase/mod.rs index 7f6e5945..e5a820f4 100644 --- a/src/transaction_signature_options/sign_with_seed_phrase/mod.rs +++ b/src/transaction_signature_options/sign_with_seed_phrase/mod.rs @@ -213,10 +213,15 @@ impl SignSeedPhrase { fn input_seed_phrase_hd_path( _context: &crate::commands::TransactionContext, ) -> color_eyre::eyre::Result> { - Ok(Some( - CustomType::new("Enter seed phrase HD Path (if not sure, keep the default):") - .with_starting_input("m/44'/397'/0'") - .prompt()?, - )) + input_seed_phrase_hd_path() } } + +pub fn input_seed_phrase_hd_path( +) -> color_eyre::eyre::Result> { + Ok(Some( + CustomType::new("Enter seed phrase HD Path (if not sure, keep the default):") + .with_starting_input("m/44'/397'/0'") + .prompt()?, + )) +}