Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions src/commands/account/get_public_key/from_keychain/mod.rs
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 {
Copy link
Collaborator

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....

Copy link
Collaborator Author

@FroVolod FroVolod Jan 17, 2025

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

#[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
}
}
54 changes: 54 additions & 0 deletions src/commands/account/get_public_key/from_ledger/mod.rs
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()
}
}
133 changes: 133 additions & 0 deletions src/commands/account/get_public_key/from_legacy_keychain/mod.rs
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."
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.
For example, an account has 10 key files. We give the user the public key and say that we don't know if it is active or has 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
}
}
28 changes: 28 additions & 0 deletions src/commands/account/get_public_key/from_seed_phrase/mod.rs
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe, we could configure BIP32Path in other handlers

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should stay consistent here

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

@FroVolod FroVolod Jan 17, 2025

Choose a reason for hiding this comment

The 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)
}
}
42 changes: 42 additions & 0 deletions src/commands/account/get_public_key/mod.rs
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--help uses these comments:

Screenshot 2025-01-17 at 16 36 18

Copy link
Collaborator

@akorchyn akorchyn Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it use strum macro above ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strum macro processes these messages with various functions

Screenshot 2025-01-17 at 20 46 23

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),
}
6 changes: 6 additions & 0 deletions src/commands/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"
))]
Expand Down
Loading
Loading