use coins_bip32::prelude::*;
use k256::ecdsa::SigningKey;
use serde::{Deserialize, Serialize, Serializer};
use tiny_keccak::{Hasher, Keccak};
use crate::{Error, Result};
#[derive(Debug, Clone, Deserialize)]
pub struct EthWallet {
#[serde(deserialize_with = "deserialize_privkey_hex")]
pub private_key: [u8; 32],
pub address: String,
}
impl Serialize for EthWallet {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("EthWallet", 2)?;
s.serialize_field("private_key", &hex::encode(self.private_key))?;
s.serialize_field("address", &self.address)?;
s.end()
}
}
fn deserialize_privkey_hex<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<[u8; 32], D::Error> {
let s = String::deserialize(deserializer)?;
let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
if bytes.len() != 32 {
return Err(serde::de::Error::custom(format!(
"expected 32 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
pub fn derive_eoa(mnemonic: &str) -> Result<EthWallet> {
let seed = crate::crypto::mnemonic_to_seed_bytes(mnemonic)?;
let master = XPriv::root_from_seed(&seed, None)
.map_err(|e| Error::Crypto(format!("BIP-32 master key failed: {}", e)))?;
let path = "m/44'/60'/0'/0/0";
let derived = master
.derive_path(path)
.map_err(|e| Error::Crypto(format!("BIP-44 derivation failed: {}", e)))?;
let derived_signing_key: &k256::ecdsa::SigningKey = derived.as_ref();
let mut private_key = [0u8; 32];
private_key.copy_from_slice(&derived_signing_key.to_bytes());
let signing_key = SigningKey::from_bytes((&private_key).into())
.map_err(|e| Error::Crypto(format!("Invalid private key: {}", e)))?;
let verifying_key = signing_key.verifying_key();
let public_key_bytes = verifying_key.to_encoded_point(false);
let pubkey_raw = &public_key_bytes.as_bytes()[1..];
let mut keccak = Keccak::v256();
let mut hash = [0u8; 32];
keccak.update(pubkey_raw);
keccak.finalize(&mut hash);
let address = format!("0x{}", hex::encode(&hash[12..]));
Ok(EthWallet {
private_key,
address,
})
}
pub fn derive_eoa_address(mnemonic: &str) -> Result<String> {
Ok(derive_eoa(mnemonic)?.address)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eoa_derivation_parity() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let w = derive_eoa(mnemonic).unwrap();
assert_eq!(
hex::encode(&w.private_key),
"1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727",
"Private key must match reference derivation"
);
assert_eq!(
w.address.to_lowercase(),
"0x9858effd232b4033e47d90003d41ec34ecaeda94",
"EOA must match eth_account.from_mnemonic for the 12-word test mnemonic"
);
}
#[test]
fn test_derive_eoa_address() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let address = derive_eoa_address(mnemonic).unwrap();
assert_eq!(
address.to_lowercase(),
"0x9858effd232b4033e47d90003d41ec34ecaeda94"
);
}
#[test]
fn test_ethwallet_serialization() {
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let w = derive_eoa(mnemonic).unwrap();
let json = serde_json::to_string(&w).unwrap();
assert!(json.contains("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"));
assert!(json.contains("0x9858effd232b4033e47d90003d41ec34ecaeda94"));
let w2: EthWallet = serde_json::from_str(&json).unwrap();
assert_eq!(w.private_key, w2.private_key);
assert_eq!(w.address, w2.address);
}
}