-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Added the ability to get a public key for an account #436
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: &<PublicKeyFromKeychain as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope, | ||
) -> color_eyre::eyre::Result<Self> { | ||
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<Option<crate::types::account_id::AccountId>> { | ||
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<PublicKeyFromKeychainContext> for crate::network::NetworkContext { | ||
fn from(item: PublicKeyFromKeychainContext) -> Self { | ||
item.0 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
#[derive(Debug, Clone, interactive_clap::InteractiveClap)] | ||
#[interactive_clap(input_context = crate::GlobalContext)] | ||
#[interactive_clap(output_context = PublicKeyFromLedgerContext)] | ||
pub struct PublicKeyFromLedger { | ||
#[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: &<PublicKeyFromLedger as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope, | ||
) -> color_eyre::eyre::Result<Self> { | ||
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<Option<crate::types::slip10::BIP32Path>> { | ||
crate::transaction_signature_options::sign_with_ledger::input_seed_phrase_hd_path() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: &<PublicKeyFromKeychain as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope, | ||
) -> color_eyre::eyre::Result<Self> { | ||
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." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But why ? I still think we can easily process the directory and return user a key. Just with a warning that this key wasn't checked that it's active on chain There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can easily process the directory and return the key to the user. But we promised the user that they would be able to get the public key with full access. |
||
); | ||
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::<std::collections::HashSet<std::ffi::OsString>>(); | ||
|
||
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<Option<crate::types::account_id::AccountId>> { | ||
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<PublicKeyFromLegacyKeychainContext> for crate::network::NetworkContext { | ||
fn from(item: PublicKeyFromLegacyKeychainContext) -> Self { | ||
item.0 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
use std::str::FromStr; | ||
|
||
#[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, | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct PublicKeyFromSeedPhraseContext; | ||
|
||
impl PublicKeyFromSeedPhraseContext { | ||
pub fn from_previous_context( | ||
_previous_context: crate::GlobalContext, | ||
scope: &<PublicKeyFromSeedPhrase as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope, | ||
) -> color_eyre::eyre::Result<Self> { | ||
let seed_phrase_hd_path_default = slipped10::BIP32Path::from_str("m/44'/397'/0'").unwrap(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe, we could configure BIP32Path in other handlers There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should stay consistent here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Example. It actuallyt seems to be skipped, but we can at least have a structure similar way There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
let public_key = crate::common::get_public_key_from_seed_phrase( | ||
seed_phrase_hd_path_default, | ||
&scope.master_seed_phrase, | ||
)?; | ||
eprintln!("\nPublic key: {}", public_key); | ||
|
||
Ok(Self) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if these comments have any value as they are repeating text from the message above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't it use strum macro above ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit weird, to be honest. Maybe we shouldn't have that. Because to get a key from a keychain, we need to fetch a key before-hand (e.g., from the chain). So it doesn't really make sense.
Maybe we can iterate somehow over keychain entries and parse it that way....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it works:
Screen.Recording.2025-01-17.at.21.01.13.mov