Skip to content

Commit

Permalink
feat!: NEP-413 support (#37)
Browse files Browse the repository at this point in the history
* feat: initial implementation of NEP-413 support

* nep413 tested and works

* removed redundant doc line

* added example for nep413

* doctest

* renamed method

* added test suite without callback

* review
  • Loading branch information
akorchyn authored Jan 17, 2025
1 parent 68f7050 commit 65d4b8c
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 172 deletions.
33 changes: 33 additions & 0 deletions examples/nep_413_signing_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use near_api::*;

use openssl::rand::rand_bytes;

#[tokio::main]
async fn main() {
let signer = Signer::from_seed_phrase(
"fatal edge jacket cash hard pass gallery fabric whisper size rain biology",
None,
)
.unwrap();

let mut nonce = [0u8; 32];
rand_bytes(&mut nonce).unwrap();

let payload = near_api::signer::NEP413Payload {
message: "Hello NEAR!".to_string(),
nonce,
recipient: "example.near".to_string(),
callback_url: None,
};

let signature = signer
.sign_message_nep413(
"round-toad.testnet".parse().unwrap(),
signer.get_public_key().unwrap(),
payload,
)
.await
.unwrap();

println!("Signature: {}", signature);
}
6 changes: 6 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub enum SignerError {
SecretKeyIsNotAvailable,
#[error("Failed to fetch nonce: {0}")]
FetchNonceError(#[from] QueryError<RpcQueryRequest>),
#[error("IO error: {0}")]
IO(#[from] std::io::Error),

#[cfg(feature = "ledger")]
#[error(transparent)]
Expand All @@ -70,6 +72,8 @@ pub enum AccessKeyFileError {
ParseError(#[from] serde_json::Error),
#[error(transparent)]
SecretError(#[from] SecretError),
#[error("Public key is not linked to the private key")]
PrivatePublicKeyMismatch,
}

#[cfg(feature = "keystore")]
Expand All @@ -83,6 +87,8 @@ pub enum KeyStoreError {
ParseError(#[from] serde_json::Error),
#[error(transparent)]
SecretError(#[from] SecretError),
#[error("Task execution error: {0}")]
TaskExecutionError(#[from] tokio::task::JoinError),
}

#[cfg(feature = "ledger")]
Expand Down
59 changes: 0 additions & 59 deletions src/signer/access_keyfile_signer.rs

This file was deleted.

78 changes: 37 additions & 41 deletions src/signer/keystore.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
use futures::future::join_all;
use near_crypto::{PublicKey, SecretKey};
use near_primitives::{
transaction::Transaction,
types::{AccountId, Nonce},
views::AccessKeyPermissionView,
};
use near_primitives::{types::AccountId, views::AccessKeyPermissionView};
use tracing::{debug, info, instrument, trace, warn};

use crate::{
config::NetworkConfig,
errors::{KeyStoreError, SignerError},
types::{transactions::PrepopulateTransaction, CryptoHash},
};

use super::{AccountKeyPair, SignerTrait};
Expand All @@ -23,38 +19,31 @@ pub struct KeystoreSigner {

#[async_trait::async_trait]
impl SignerTrait for KeystoreSigner {
#[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))]
fn tx_and_secret(
#[instrument(skip(self))]
async fn get_secret_key(
&self,
tr: PrepopulateTransaction,
public_key: PublicKey,
nonce: Nonce,
block_hash: CryptoHash,
) -> Result<(Transaction, SecretKey), SignerError> {
signer_id: &AccountId,
public_key: &PublicKey,
) -> Result<SecretKey, SignerError> {
debug!(target: KEYSTORE_SIGNER_TARGET, "Searching for matching public key");
self.potential_pubkeys
.iter()
.find(|key| *key == &public_key)
.find(|key| *key == public_key)
.ok_or(SignerError::PublicKeyIsNotAvailable)?;

info!(target: KEYSTORE_SIGNER_TARGET, "Retrieving secret key");
// TODO: fix this. Well the search is a bit suboptimal, but it's not a big deal for now
let secret = Self::get_secret_key(&tr.signer_id, &public_key, "mainnet")
.or_else(|_| Self::get_secret_key(&tr.signer_id, &public_key, "testnet"))
.map_err(|_| SignerError::SecretKeyIsNotAvailable)?;

debug!(target: KEYSTORE_SIGNER_TARGET, "Creating transaction");
let mut transaction = Transaction::new_v0(
tr.signer_id.clone(),
public_key,
tr.receiver_id,
nonce,
block_hash.into(),
);
*transaction.actions_mut() = tr.actions;

info!(target: KEYSTORE_SIGNER_TARGET, "Transaction and secret key prepared successfully");
Ok((transaction, secret.private_key))
let secret =
if let Ok(secret) = Self::get_secret_key(signer_id, public_key, "mainnet").await {
secret
} else {
Self::get_secret_key(signer_id, public_key, "testnet")
.await
.map_err(|_| SignerError::SecretKeyIsNotAvailable)?
};

info!(target: KEYSTORE_SIGNER_TARGET, "Secret key prepared successfully");
Ok(secret.private_key)
}

#[instrument(skip(self))]
Expand Down Expand Up @@ -87,35 +76,42 @@ impl KeystoreSigner {
.await?;

debug!(target: KEYSTORE_SIGNER_TARGET, "Filtering and collecting potential public keys");
let potential_pubkeys: Vec<PublicKey> = account_keys
let potential_pubkeys = account_keys
.keys
.into_iter()
.iter()
// TODO: support functional access keys
.filter(|key| key.access_key.permission == AccessKeyPermissionView::FullAccess)
.flat_map(|key| {
Self::get_secret_key(&account_id, &key.public_key, &network.network_name)
.map(|keypair| keypair.public_key)
.ok()
})
.map(|key| Self::get_secret_key(&account_id, &key.public_key, &network.network_name));
let potential_pubkeys: Vec<PublicKey> = join_all(potential_pubkeys)
.await
.into_iter()
.flat_map(|result| result.map(|keypair| keypair.public_key).ok())
.collect();

info!(target: KEYSTORE_SIGNER_TARGET, "KeystoreSigner created with {} potential public keys", potential_pubkeys.len());
Ok(Self { potential_pubkeys })
}

#[instrument(skip(public_key), fields(account_id = %account_id, network_name = %network_name))]
fn get_secret_key(
async fn get_secret_key(
account_id: &AccountId,
public_key: &PublicKey,
network_name: &str,
) -> Result<AccountKeyPair, KeyStoreError> {
trace!(target: KEYSTORE_SIGNER_TARGET, "Retrieving secret key from keyring");
let service_name =
std::borrow::Cow::Owned(format!("near-{}-{}", network_name, account_id.as_str()));
let user = format!("{}:{}", account_id, public_key);

// This can be a blocking operation (for example, if the keyring is locked in the OS and user needs to unlock it),
// so we need to spawn a new task to get the password
let password = tokio::task::spawn_blocking(move || {
let password = keyring::Entry::new(&service_name, &user)?.get_password()?;

let password =
keyring::Entry::new(&service_name, &format!("{}:{}", account_id, public_key))?
.get_password()?;
Ok::<_, KeyStoreError>(password)
})
.await
.unwrap_or_else(|tokio_join_error| Err(KeyStoreError::from(tokio_join_error)))?;

debug!(target: KEYSTORE_SIGNER_TARGET, "Deserializing account key pair");
Ok(serde_json::from_str(&password)?)
Expand Down
38 changes: 32 additions & 6 deletions src/signer/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
types::{transactions::PrepopulateTransaction, CryptoHash},
};

use super::SignerTrait;
use super::{NEP413Payload, SignerTrait};

const LEDGER_SIGNER_TARGET: &str = "near_api::signer::ledger";

Expand Down Expand Up @@ -130,13 +130,39 @@ impl SignerTrait for LedgerSigner {
})
}

fn tx_and_secret(
#[instrument(skip(self), fields(signer_id = %_signer_id, receiver_id = %payload.recipient, message = %payload.message))]
async fn sign_message_nep413(
&self,
_tr: PrepopulateTransaction,
_signer_id: crate::AccountId,
_public_key: PublicKey,
_nonce: Nonce,
_block_hash: CryptoHash,
) -> Result<(Transaction, SecretKey), SignerError> {
payload: NEP413Payload,
) -> Result<near_crypto::Signature, SignerError> {
info!(target: LEDGER_SIGNER_TARGET, "Signing NEP413 message with Ledger");
let hd_path = self.hd_path.clone();
let payload = payload.into();

let signature: Vec<u8> = tokio::task::spawn_blocking(move || {
let signature =
near_ledger::sign_message_nep413(&payload, hd_path).map_err(LedgerError::from)?;

Ok::<_, LedgerError>(signature)
})
.await
.unwrap_or_else(|tokio_join_error| Err(LedgerError::from(tokio_join_error)))?;

debug!(target: LEDGER_SIGNER_TARGET, "Creating Signature object for NEP413");
let signature =
near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature)
.map_err(LedgerError::from)?;

Ok(signature)
}

async fn get_secret_key(
&self,
_signer_id: &crate::AccountId,
_public_key: &PublicKey,
) -> Result<SecretKey, SignerError> {
warn!(target: LEDGER_SIGNER_TARGET, "Attempted to access secret key, which is not available for Ledger signer");
Err(SignerError::SecretKeyIsNotAvailable)
}
Expand Down
Loading

0 comments on commit 65d4b8c

Please sign in to comment.