use std::future::Future;
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::error::SignerError;
use crate::types::nep413::{self, SignMessageParams, SignedMessage};
use crate::types::{AccountId, PublicKey, SecretKey, Signature, TryIntoAccountId};
pub trait Signer: Send + Sync {
fn account_id(&self) -> &AccountId;
fn key(&self) -> SigningKey;
fn public_key(&self) -> PublicKey {
self.key().public_key().clone()
}
}
impl<T: Signer + ?Sized> Signer for Arc<T> {
fn account_id(&self) -> &AccountId {
(**self).account_id()
}
fn key(&self) -> SigningKey {
(**self).key()
}
fn public_key(&self) -> PublicKey {
(**self).public_key()
}
}
pub struct SigningKey {
public_key: PublicKey,
backend: Arc<dyn SigningBackend>,
}
impl SigningKey {
pub fn new(secret_key: SecretKey) -> Self {
let public_key = secret_key.public_key();
Self {
public_key,
backend: Arc::new(SecretKeyBackend { secret_key }),
}
}
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
pub async fn sign(&self, message: &[u8]) -> Result<Signature, SignerError> {
self.backend.sign(message).await
}
pub async fn sign_nep413(
&self,
account_id: &AccountId,
params: &SignMessageParams,
) -> Result<SignedMessage, SignerError> {
let hash = nep413::serialize_message(params);
let signature = self.sign(hash.as_bytes()).await?;
Ok(SignedMessage {
account_id: account_id.clone(),
public_key: self.public_key.clone(),
signature,
state: params.state.clone(),
})
}
}
impl Clone for SigningKey {
fn clone(&self) -> Self {
Self {
public_key: self.public_key.clone(),
backend: self.backend.clone(),
}
}
}
impl std::fmt::Debug for SigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SigningKey")
.field("public_key", &self.public_key)
.finish()
}
}
trait SigningBackend: Send + Sync {
fn sign(
&self,
message: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Signature, SignerError>> + Send + '_>>;
}
struct SecretKeyBackend {
secret_key: SecretKey,
}
impl SigningBackend for SecretKeyBackend {
fn sign(
&self,
message: &[u8],
) -> Pin<Box<dyn Future<Output = Result<Signature, SignerError>> + Send + '_>> {
let sig = self.secret_key.sign(message);
Box::pin(async move { Ok(sig) })
}
}
#[derive(Clone)]
pub struct InMemorySigner {
account_id: AccountId,
secret_key: SecretKey,
public_key: PublicKey,
}
impl InMemorySigner {
pub fn new(
account_id: impl TryIntoAccountId,
secret_key: impl AsRef<str>,
) -> Result<Self, crate::error::Error> {
let account_id: AccountId = account_id.try_into_account_id()?;
let secret_key: SecretKey = secret_key.as_ref().parse()?;
let public_key = secret_key.public_key();
Ok(Self {
account_id,
secret_key,
public_key,
})
}
pub fn from_secret_key(
account_id: impl TryIntoAccountId,
secret_key: SecretKey,
) -> Result<Self, crate::error::Error> {
let account_id: AccountId = account_id.try_into_account_id()?;
let public_key = secret_key.public_key();
Ok(Self {
account_id,
secret_key,
public_key,
})
}
pub fn generate_implicit() -> Self {
let secret_key = SecretKey::generate_ed25519();
Self::implicit(secret_key)
}
pub fn implicit(secret_key: SecretKey) -> Self {
let public_key = secret_key.public_key();
let pk_bytes = public_key
.as_ed25519_bytes()
.expect("implicit accounts require an Ed25519 key");
let account_id: AccountId = hex::encode(pk_bytes)
.parse()
.expect("hex-encoded Ed25519 public key is a valid account ID");
Self {
account_id,
secret_key,
public_key,
}
}
pub fn from_seed_phrase(
account_id: impl TryIntoAccountId,
phrase: impl AsRef<str>,
) -> Result<Self, crate::error::Error> {
let secret_key = SecretKey::from_seed_phrase(phrase)?;
Self::from_secret_key(account_id, secret_key)
}
pub fn from_seed_phrase_with_path(
account_id: impl TryIntoAccountId,
phrase: impl AsRef<str>,
hd_path: impl AsRef<str>,
) -> Result<Self, crate::error::Error> {
let secret_key = SecretKey::from_seed_phrase_with_path(phrase, hd_path)?;
Self::from_secret_key(account_id, secret_key)
}
pub fn public_key(&self) -> &PublicKey {
&self.public_key
}
}
impl std::fmt::Debug for InMemorySigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InMemorySigner")
.field("account_id", &self.account_id)
.field("public_key", &self.public_key)
.finish()
}
}
impl Signer for InMemorySigner {
fn account_id(&self) -> &AccountId {
&self.account_id
}
fn key(&self) -> SigningKey {
SigningKey::new(self.secret_key.clone())
}
}
#[derive(Clone)]
pub struct FileSigner {
inner: InMemorySigner,
}
#[derive(serde::Deserialize)]
struct CredentialFile {
#[serde(alias = "secret_key")]
private_key: String,
}
impl FileSigner {
pub fn new(
network: impl AsRef<str>,
account_id: impl TryIntoAccountId,
) -> Result<Self, crate::error::Error> {
let account_id: AccountId = account_id.try_into_account_id()?;
let home = dirs::home_dir().ok_or_else(|| {
crate::error::Error::Config("Could not determine home directory".to_string())
})?;
let path = home
.join(".near-credentials")
.join(network.as_ref())
.join(format!("{}.json", account_id));
Self::from_file(&path, account_id)
}
pub fn from_file(
path: impl AsRef<Path>,
account_id: impl TryIntoAccountId,
) -> Result<Self, crate::error::Error> {
let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
crate::error::Error::Config(format!(
"Failed to read credentials file {}: {}",
path.as_ref().display(),
e
))
})?;
let cred: CredentialFile = serde_json::from_str(&content).map_err(|e| {
crate::error::Error::Config(format!(
"Failed to parse credentials file {}: {}",
path.as_ref().display(),
e
))
})?;
let inner = InMemorySigner::new(account_id, &cred.private_key)?;
Ok(Self { inner })
}
pub fn public_key(&self) -> &PublicKey {
self.inner.public_key()
}
pub fn into_inner(self) -> InMemorySigner {
self.inner
}
}
impl std::fmt::Debug for FileSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileSigner")
.field("account_id", &self.inner.account_id)
.field("public_key", &self.inner.public_key)
.finish()
}
}
impl Signer for FileSigner {
fn account_id(&self) -> &AccountId {
self.inner.account_id()
}
fn key(&self) -> SigningKey {
self.inner.key()
}
}
#[derive(Clone)]
pub struct EnvSigner {
inner: InMemorySigner,
}
impl EnvSigner {
pub fn new() -> Result<Self, crate::error::Error> {
Self::from_env_vars("NEAR_ACCOUNT_ID", "NEAR_PRIVATE_KEY")
}
pub fn from_env_vars(account_var: &str, key_var: &str) -> Result<Self, crate::error::Error> {
let account_id = std::env::var(account_var).map_err(|_| {
crate::error::Error::Config(format!("Environment variable {} not set", account_var))
})?;
let private_key = std::env::var(key_var).map_err(|_| {
crate::error::Error::Config(format!("Environment variable {} not set", key_var))
})?;
let inner = InMemorySigner::new(account_id, &private_key)?;
Ok(Self { inner })
}
pub fn public_key(&self) -> &PublicKey {
self.inner.public_key()
}
pub fn into_inner(self) -> InMemorySigner {
self.inner
}
}
impl std::fmt::Debug for EnvSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EnvSigner")
.field("account_id", &self.inner.account_id)
.field("public_key", &self.inner.public_key)
.finish()
}
}
impl Signer for EnvSigner {
fn account_id(&self) -> &AccountId {
self.inner.account_id()
}
fn key(&self) -> SigningKey {
self.inner.key()
}
}
pub struct RotatingSigner {
account_id: AccountId,
keys: Vec<SecretKey>,
counter: AtomicUsize,
}
impl RotatingSigner {
pub fn new(
account_id: impl TryIntoAccountId,
keys: Vec<SecretKey>,
) -> Result<Self, crate::error::Error> {
if keys.is_empty() {
return Err(crate::error::Error::Config(
"RotatingSigner requires at least one key".to_string(),
));
}
let account_id: AccountId = account_id.try_into_account_id()?;
Ok(Self {
account_id,
keys,
counter: AtomicUsize::new(0),
})
}
pub fn from_signers(signers: Vec<InMemorySigner>) -> Result<Self, crate::error::Error> {
if signers.is_empty() {
return Err(crate::error::Error::Config(
"RotatingSigner requires at least one signer".to_string(),
));
}
let account_id = signers[0].account_id().clone();
for signer in &signers[1..] {
if signer.account_id() != &account_id {
return Err(crate::error::Error::Config(format!(
"All signers must share the same account ID, got {} and {}",
account_id,
signer.account_id()
)));
}
}
let keys = signers.into_iter().map(|s| s.secret_key).collect();
Ok(Self {
account_id,
keys,
counter: AtomicUsize::new(0),
})
}
pub fn from_key_strings(
account_id: impl TryIntoAccountId,
keys: &[impl AsRef<str>],
) -> Result<Self, crate::error::Error> {
let parsed_keys: Result<Vec<SecretKey>, _> =
keys.iter().map(|k| k.as_ref().parse()).collect();
Self::new(account_id, parsed_keys?)
}
pub fn key_count(&self) -> usize {
self.keys.len()
}
pub fn public_keys(&self) -> Vec<PublicKey> {
self.keys.iter().map(|sk| sk.public_key()).collect()
}
pub fn signing_keys(&self) -> Vec<SigningKey> {
self.keys
.iter()
.map(|sk| SigningKey::new(sk.clone()))
.collect()
}
pub fn into_per_key_signers(self) -> Vec<InMemorySigner> {
let account_id = self.account_id;
self.keys
.into_iter()
.map(|sk| {
InMemorySigner::from_secret_key(account_id.clone(), sk)
.expect("AccountId is always a valid account ID")
})
.collect()
}
}
impl std::fmt::Debug for RotatingSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RotatingSigner")
.field("account_id", &self.account_id)
.field("key_count", &self.keys.len())
.field("counter", &self.counter.load(Ordering::Relaxed))
.finish()
}
}
impl Signer for RotatingSigner {
fn account_id(&self) -> &AccountId {
&self.account_id
}
fn key(&self) -> SigningKey {
let idx = self.counter.fetch_add(1, Ordering::Relaxed) % self.keys.len();
SigningKey::new(self.keys[idx].clone())
}
fn public_key(&self) -> PublicKey {
let idx = self.counter.load(Ordering::Relaxed) % self.keys.len();
self.keys[idx].public_key()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Action, CryptoHash, NearToken, Transaction};
#[tokio::test]
async fn test_in_memory_signer() {
let signer = InMemorySigner::new(
"alice.testnet",
"ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
)
.unwrap();
assert_eq!(signer.account_id().as_str(), "alice.testnet");
let key = signer.key();
let message = b"test message";
let signature = key.sign(message).await.unwrap();
assert_eq!(key.public_key(), signer.public_key());
assert!(!signature.as_bytes().is_empty());
}
#[tokio::test]
async fn test_signature_consistency() {
let signer = InMemorySigner::new(
"alice.testnet",
"ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
)
.unwrap();
let key = signer.key();
let message = b"test message";
let sig1 = key.sign(message).await.unwrap();
let sig2 = key.sign(message).await.unwrap();
assert_eq!(sig1.as_bytes(), sig2.as_bytes());
}
#[tokio::test]
async fn test_different_messages_different_signatures() {
let signer = InMemorySigner::new(
"alice.testnet",
"ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
)
.unwrap();
let key = signer.key();
let sig1 = key.sign(b"message 1").await.unwrap();
let sig2 = key.sign(b"message 2").await.unwrap();
assert_ne!(sig1.as_bytes(), sig2.as_bytes());
}
#[tokio::test]
async fn test_transaction_signing_with_signer_trait() {
let secret_key = SecretKey::generate_ed25519();
let signer = InMemorySigner::from_secret_key("alice.testnet", secret_key).unwrap();
let key = signer.key();
let tx = Transaction::new(
signer.account_id().clone(),
key.public_key().clone(),
1,
"bob.testnet".parse().unwrap(),
CryptoHash::ZERO,
vec![Action::transfer(NearToken::from_near(1))],
);
let tx_hash = tx.get_hash();
let signature = key.sign(tx_hash.as_bytes()).await.unwrap();
assert_eq!(signature.as_bytes().len(), 64);
let signed_tx = crate::types::SignedTransaction {
transaction: tx,
signature,
};
let bytes = signed_tx.to_bytes();
assert!(!bytes.is_empty());
let base64 = signed_tx.to_base64();
assert!(!base64.is_empty());
}
#[tokio::test]
async fn test_rotating_signer() {
let keys = vec![
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
];
let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
let key1 = signer.key();
assert_eq!(key1.public_key(), &expected_public_keys[0]);
let key2 = signer.key();
assert_eq!(key2.public_key(), &expected_public_keys[1]);
let key3 = signer.key();
assert_eq!(key3.public_key(), &expected_public_keys[2]);
let key4 = signer.key();
assert_eq!(key4.public_key(), &expected_public_keys[0]);
}
#[tokio::test]
async fn test_rotating_signer_atomic_key_claiming() {
let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
let expected_pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
let signer = RotatingSigner::new("bot.testnet", keys.clone()).unwrap();
let message = b"test";
let key1 = signer.key();
assert_eq!(key1.public_key(), &expected_pks[0]);
let sig1 = key1.sign(message).await.unwrap();
let expected_sig1 = keys[0].sign(message);
assert_eq!(sig1.as_bytes(), expected_sig1.as_bytes());
let key2 = signer.key();
assert_eq!(key2.public_key(), &expected_pks[1]);
let sig2 = key2.sign(message).await.unwrap();
let expected_sig2 = keys[1].sign(message);
assert_eq!(sig2.as_bytes(), expected_sig2.as_bytes());
assert_ne!(sig1.as_bytes(), sig2.as_bytes());
}
#[test]
fn test_rotating_signer_empty_keys() {
let result = RotatingSigner::new("bot.testnet", vec![]);
assert!(result.is_err());
}
#[test]
fn test_env_signer_missing_vars() {
let result = EnvSigner::from_env_vars("NONEXISTENT_VAR_1", "NONEXISTENT_VAR_2");
assert!(result.is_err());
}
#[test]
fn test_signer_from_secret_key() {
let secret = SecretKey::generate_ed25519();
let expected_pk = secret.public_key();
let signer = InMemorySigner::from_secret_key("alice.testnet", secret).unwrap();
assert_eq!(signer.account_id().as_str(), "alice.testnet");
assert_eq!(signer.public_key(), &expected_pk);
}
#[test]
fn test_from_secret_key_accepts_string() {
let secret = SecretKey::generate_ed25519();
let signer = InMemorySigner::from_secret_key("alice.testnet", secret);
assert!(signer.is_ok());
}
#[test]
fn test_from_secret_key_rejects_invalid_account() {
let secret = SecretKey::generate_ed25519();
let signer = InMemorySigner::from_secret_key("", secret);
assert!(signer.is_err());
}
#[test]
fn test_generate_implicit() {
let signer = InMemorySigner::generate_implicit();
let account_id = signer.account_id().as_str();
assert_eq!(account_id.len(), 64);
assert!(account_id.chars().all(|c| c.is_ascii_hexdigit()));
let pk_bytes = signer.public_key().as_ed25519_bytes().unwrap();
assert_eq!(account_id, hex::encode(pk_bytes));
}
#[test]
fn test_implicit() {
let secret_key = SecretKey::generate_ed25519();
let expected_pk = secret_key.public_key();
let signer = InMemorySigner::implicit(secret_key);
let pk_bytes = expected_pk.as_ed25519_bytes().unwrap();
assert_eq!(signer.account_id().as_str(), hex::encode(pk_bytes));
assert_eq!(signer.public_key(), &expected_pk);
}
#[test]
fn test_generate_implicit_unique() {
let signer1 = InMemorySigner::generate_implicit();
let signer2 = InMemorySigner::generate_implicit();
assert_ne!(signer1.account_id(), signer2.account_id());
}
#[test]
fn test_rotating_signer_key_count() {
let keys = vec![
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
];
let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
assert_eq!(signer.key_count(), 3);
assert_eq!(signer.public_keys().len(), 3);
}
#[test]
fn test_rotating_signer_from_key_strings() {
let keys = [
"ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
"ed25519:4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi5kL6YJt9Z6iLqMBkVfqDH2Zj8bxqXTdMkNmvPcAD8LqCZ",
];
let signer = RotatingSigner::from_key_strings("bot.testnet", &keys).unwrap();
assert_eq!(signer.key_count(), 2);
assert_eq!(signer.account_id().as_str(), "bot.testnet");
}
#[test]
fn test_in_memory_signer_debug_hides_secret() {
let signer = InMemorySigner::new(
"alice.testnet",
"ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
)
.unwrap();
let debug_str = format!("{:?}", signer);
assert!(debug_str.contains("alice.testnet"));
assert!(debug_str.contains("public_key"));
assert!(!debug_str.contains("secret_key"));
assert!(!debug_str.contains("3D4YudUahN1nawWogh"));
}
#[test]
fn test_rotating_signer_from_signers() {
let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
let signers: Vec<InMemorySigner> = keys
.into_iter()
.map(|sk| InMemorySigner::from_secret_key("bot.testnet", sk).unwrap())
.collect();
let rotating = RotatingSigner::from_signers(signers).unwrap();
assert_eq!(rotating.key_count(), 2);
assert_eq!(rotating.public_keys(), expected_public_keys);
}
#[test]
fn test_rotating_signer_from_signers_mismatched_accounts() {
let signers = vec![
InMemorySigner::from_secret_key("alice.testnet", SecretKey::generate_ed25519())
.unwrap(),
InMemorySigner::from_secret_key("bob.testnet", SecretKey::generate_ed25519()).unwrap(),
];
let result = RotatingSigner::from_signers(signers);
assert!(result.is_err());
}
#[test]
fn test_rotating_signer_signing_keys() {
let keys = vec![
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
];
let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
let counter_before = signer.counter.load(Ordering::Relaxed);
let signing_keys = signer.signing_keys();
let counter_after = signer.counter.load(Ordering::Relaxed);
assert_eq!(counter_before, counter_after);
assert_eq!(signing_keys.len(), 3);
for (sk, expected_pk) in signing_keys.iter().zip(&expected_public_keys) {
assert_eq!(sk.public_key(), expected_pk);
}
}
#[test]
fn test_rotating_signer_into_per_key_signers() {
let keys = vec![SecretKey::generate_ed25519(), SecretKey::generate_ed25519()];
let expected_public_keys: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
let per_key = signer.into_per_key_signers();
assert_eq!(per_key.len(), 2);
for (ims, expected_pk) in per_key.iter().zip(&expected_public_keys) {
assert_eq!(ims.account_id().as_str(), "bot.testnet");
assert_eq!(ims.public_key(), expected_pk);
}
}
#[test]
fn test_rotating_signer_public_key_no_side_effect() {
let keys = vec![
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
SecretKey::generate_ed25519(),
];
let pks: Vec<_> = keys.iter().map(|k| k.public_key()).collect();
let signer = RotatingSigner::new("bot.testnet", keys).unwrap();
assert_eq!(signer.public_key(), pks[0]);
assert_eq!(signer.public_key(), pks[0]);
assert_eq!(signer.public_key(), pks[0]);
let claimed = signer.key();
assert_eq!(claimed.public_key(), &pks[0]);
assert_eq!(signer.public_key(), pks[1]);
let claimed = signer.key();
assert_eq!(claimed.public_key(), &pks[1]);
assert_eq!(signer.public_key(), pks[2]);
let claimed = signer.key();
assert_eq!(claimed.public_key(), &pks[2]);
assert_eq!(signer.public_key(), pks[0]);
}
#[test]
fn test_signing_key_is_clone() {
let signer = InMemorySigner::new(
"alice.testnet",
"ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr",
)
.unwrap();
let key = signer.key();
let key_clone = key.clone();
assert_eq!(key.public_key(), key_clone.public_key());
}
}