use core::fmt;
use k256::ecdsa::SigningKey;
use serde::{Deserialize, Serialize};
use tiny_keccak::{Hasher, Keccak};
use crate::error::ClientError;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Address(#[serde(with = "hex_array_20")] pub [u8; 20]);
impl Address {
pub const ZERO: Self = Self([0u8; 20]);
#[must_use]
pub const fn from_bytes(bytes: [u8; 20]) -> Self {
Self(bytes)
}
pub fn from_hex(s: &str) -> Result<Self, ClientError> {
let stripped = s.strip_prefix("0x").unwrap_or(s);
if stripped.len() != 40 {
return Err(ClientError::InvalidKey(format!(
"address hex must be 40 chars, got {}",
stripped.len()
)));
}
let bytes = hex::decode(stripped)
.map_err(|e| ClientError::InvalidKey(format!("address hex decode: {e}")))?;
let mut out = [0u8; 20];
out.copy_from_slice(&bytes);
Ok(Self(out))
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 20] {
&self.0
}
}
impl fmt::Debug for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{}", hex::encode(self.0))
}
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{}", hex::encode(self.0))
}
}
mod hex_array_20 {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8; 20], s: S) -> Result<S::Ok, S::Error> {
let hex_str = format!("0x{}", hex::encode(bytes));
s.serialize_str(&hex_str)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 20], D::Error> {
let s: String = String::deserialize(d)?;
let stripped = s.strip_prefix("0x").unwrap_or(&s);
if stripped.len() != 40 {
return Err(serde::de::Error::custom(format!(
"address hex must be 40 chars, got {}",
stripped.len()
)));
}
let bytes = hex::decode(stripped).map_err(serde::de::Error::custom)?;
let mut out = [0u8; 20];
out.copy_from_slice(&bytes);
Ok(out)
}
}
#[derive(Clone)]
pub struct Wallet {
key: SigningKey,
address: Address,
}
impl Wallet {
pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, ClientError> {
let key = SigningKey::from_bytes(&bytes.into())
.map_err(|e| ClientError::InvalidKey(e.to_string()))?;
let address = address_from_signing_key(&key);
Ok(Self { key, address })
}
pub fn from_hex(s: &str) -> Result<Self, ClientError> {
let stripped = s.strip_prefix("0x").unwrap_or(s);
if stripped.len() != 64 {
return Err(ClientError::InvalidKey(format!(
"private key hex must be 64 chars, got {}",
stripped.len()
)));
}
let raw = hex::decode(stripped)
.map_err(|e| ClientError::InvalidKey(format!("private key hex decode: {e}")))?;
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&raw);
Self::from_bytes(bytes)
}
#[doc(hidden)]
#[must_use]
pub fn random_for_testing() -> Self {
let key = SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
let address = address_from_signing_key(&key);
Self { key, address }
}
#[must_use]
pub const fn address(&self) -> Address {
self.address
}
pub(crate) const fn signing_key(&self) -> &SigningKey {
&self.key
}
}
impl fmt::Debug for Wallet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Wallet")
.field("address", &self.address)
.field("key", &"<redacted>")
.finish()
}
}
fn address_from_signing_key(key: &SigningKey) -> Address {
let verifying = key.verifying_key();
let point = verifying.to_encoded_point( false);
let pubkey_bytes = point.as_bytes(); debug_assert_eq!(pubkey_bytes.len(), 65);
debug_assert_eq!(pubkey_bytes[0], 0x04);
let mut hasher = Keccak::v256();
hasher.update(&pubkey_bytes[1..]); let mut digest = [0u8; 32];
hasher.finalize(&mut digest);
let mut addr = [0u8; 20];
addr.copy_from_slice(&digest[12..32]);
Address(addr)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_vector_eip55_book_example() {
let priv_hex = "4646464646464646464646464646464646464646464646464646464646464646";
let wallet = Wallet::from_hex(priv_hex).unwrap();
let got = hex::encode(wallet.address().as_bytes());
assert_eq!(got, "9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f");
}
#[test]
fn from_hex_accepts_0x_prefix() {
let a =
Wallet::from_hex("0x4646464646464646464646464646464646464646464646464646464646464646")
.unwrap();
let b =
Wallet::from_hex("4646464646464646464646464646464646464646464646464646464646464646")
.unwrap();
assert_eq!(a.address(), b.address());
}
#[test]
fn from_hex_rejects_short_input() {
let e = Wallet::from_hex("dead").unwrap_err();
assert!(matches!(e, ClientError::InvalidKey(_)));
}
#[test]
fn from_hex_rejects_non_hex() {
let e = Wallet::from_hex(&"z".repeat(64)).unwrap_err();
assert!(matches!(e, ClientError::InvalidKey(_)));
}
#[test]
fn address_parses_with_and_without_prefix() {
let a = Address::from_hex("0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f").unwrap();
let b = Address::from_hex("9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f").unwrap();
assert_eq!(a, b);
}
#[test]
fn address_display_lowercase_hex() {
let a = Address::from_bytes([0x9du8; 20]);
let s = format!("{a}");
assert!(s.starts_with("0x"));
assert_eq!(s.len(), 42);
}
#[test]
fn debug_redacts_secret() {
let w = Wallet::random_for_testing();
let dbg = format!("{w:?}");
assert!(dbg.contains("redacted"));
assert!(!dbg.to_lowercase().contains("signingkey"));
}
}