use crate::client::signer::{InMemorySigner, Signer, SigningKey};
use crate::error::{Error, KeyStoreError, ParseKeyError};
use crate::types::{AccountId, PublicKey, SecretKey, TryIntoAccountId};
#[derive(Clone)]
pub struct KeyringSigner {
inner: InMemorySigner,
}
impl KeyringSigner {
pub fn new(
network: impl AsRef<str>,
account_id: impl TryIntoAccountId,
public_key: impl AsRef<str>,
) -> Result<Self, Error> {
let network = network.as_ref();
let account_id: AccountId = account_id.try_into_account_id()?;
let public_key_str = public_key.as_ref();
let public_key: PublicKey = public_key_str.parse()?;
let service_name = format!("near-{}-{}", network, account_id);
let username = format!("{}:{}", account_id, public_key_str);
let entry = keyring::Entry::new(&service_name, &username).map_err(|e| {
Error::KeyStore(KeyStoreError::Platform(format!(
"Failed to access keyring: {}. On Linux, ensure a Secret Service daemon \
(gnome-keyring, kwallet) is running.",
e
)))
})?;
let password = entry.get_password().map_err(|e| match e {
keyring::Error::NoEntry => {
Error::KeyStore(KeyStoreError::KeyNotFound(account_id.clone()))
}
_ => Error::KeyStore(KeyStoreError::Platform(format!(
"Failed to read from keyring: {}",
e
))),
})?;
let secret_key = parse_keyring_credential(&password, &account_id, &public_key)?;
let inner = InMemorySigner::from_secret_key(account_id, secret_key)?;
if inner.public_key() != &public_key {
return Err(Error::KeyStore(KeyStoreError::InvalidFormat(format!(
"Public key mismatch: stored key has {}, but requested {}",
inner.public_key(),
public_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 KeyringSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyringSigner")
.field("account_id", self.inner.account_id())
.field("public_key", self.inner.public_key())
.finish()
}
}
impl Signer for KeyringSigner {
fn account_id(&self) -> &AccountId {
self.inner.account_id()
}
fn key(&self) -> SigningKey {
self.inner.key()
}
}
fn parse_keyring_credential(
json_str: &str,
account_id: &AccountId,
_public_key: &PublicKey,
) -> Result<SecretKey, Error> {
let value: serde_json::Value = serde_json::from_str(json_str).map_err(|e| {
Error::KeyStore(KeyStoreError::InvalidFormat(format!(
"Invalid JSON in keyring credential for {}: {}",
account_id, e
)))
})?;
let private_key_str = value
.get("private_key")
.and_then(|v| v.as_str())
.ok_or_else(|| {
Error::KeyStore(KeyStoreError::InvalidFormat(format!(
"Missing 'private_key' field in keyring credential for {}",
account_id
)))
})?;
let secret_key: SecretKey = private_key_str
.parse()
.map_err(|e: ParseKeyError| Error::KeyStore(KeyStoreError::InvalidKey(e)))?;
Ok(secret_key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_full_format() {
let json = r#"{
"seed_phrase_hd_path": "m/44'/397'/0'",
"master_seed_phrase": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
"implicit_account_id": "c4f5941e81e071c2fd1dae2e71fd3d859d462484391d9a90bf219211dcbb320f",
"public_key": "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847",
"private_key": "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"
}"#;
let account_id: AccountId = "alice.testnet".parse().unwrap();
let public_key: PublicKey = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847"
.parse()
.unwrap();
let secret_key = parse_keyring_credential(json, &account_id, &public_key).unwrap();
assert!(secret_key.to_string().starts_with("ed25519:"));
}
#[test]
fn test_parse_simple_format() {
let json = r#"{
"public_key": "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847",
"private_key": "ed25519:3D4YudUahN1nawWogh8pAKSj92sUNMdbZGjn7kERKzYoTy8tnFQuwoGUC51DowKqorvkr2pytJSnwuSbsNVfqygr"
}"#;
let account_id: AccountId = "alice.testnet".parse().unwrap();
let public_key: PublicKey = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847"
.parse()
.unwrap();
let secret_key = parse_keyring_credential(json, &account_id, &public_key).unwrap();
assert!(secret_key.to_string().starts_with("ed25519:"));
}
#[test]
fn test_parse_missing_private_key() {
let json = r#"{
"public_key": "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847"
}"#;
let account_id: AccountId = "alice.testnet".parse().unwrap();
let public_key: PublicKey = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847"
.parse()
.unwrap();
let result = parse_keyring_credential(json, &account_id, &public_key);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Missing 'private_key' field"));
}
#[test]
fn test_parse_invalid_json() {
let json = "not valid json";
let account_id: AccountId = "alice.testnet".parse().unwrap();
let public_key: PublicKey = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847"
.parse()
.unwrap();
let result = parse_keyring_credential(json, &account_id, &public_key);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Invalid JSON"));
}
#[test]
fn test_parse_invalid_key_format() {
let json = r#"{
"public_key": "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847",
"private_key": "not-a-valid-key"
}"#;
let account_id: AccountId = "alice.testnet".parse().unwrap();
let public_key: PublicKey = "ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847"
.parse()
.unwrap();
let result = parse_keyring_credential(json, &account_id, &public_key);
assert!(result.is_err());
}
}