totalreclaw_core/
wallet.rs1use coins_bip32::prelude::*;
9use k256::ecdsa::SigningKey;
10use serde::{Deserialize, Serialize, Serializer};
11use tiny_keccak::{Hasher, Keccak};
12
13use crate::{Error, Result};
14
15#[derive(Debug, Clone, Deserialize)]
19pub struct EthWallet {
20 #[serde(deserialize_with = "deserialize_privkey_hex")]
22 pub private_key: [u8; 32],
23 pub address: String,
25}
26
27impl Serialize for EthWallet {
28 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
29 use serde::ser::SerializeStruct;
30 let mut s = serializer.serialize_struct("EthWallet", 2)?;
31 s.serialize_field("private_key", &hex::encode(self.private_key))?;
32 s.serialize_field("address", &self.address)?;
33 s.end()
34 }
35}
36
37fn deserialize_privkey_hex<'de, D: serde::Deserializer<'de>>(
38 deserializer: D,
39) -> std::result::Result<[u8; 32], D::Error> {
40 let s = String::deserialize(deserializer)?;
41 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
42 if bytes.len() != 32 {
43 return Err(serde::de::Error::custom(format!(
44 "expected 32 bytes, got {}",
45 bytes.len()
46 )));
47 }
48 let mut arr = [0u8; 32];
49 arr.copy_from_slice(&bytes);
50 Ok(arr)
51}
52
53pub fn derive_eoa(mnemonic: &str) -> Result<EthWallet> {
58 let seed = crate::crypto::mnemonic_to_seed_bytes(mnemonic)?;
60
61 let master = XPriv::root_from_seed(&seed, None)
63 .map_err(|e| Error::Crypto(format!("BIP-32 master key failed: {}", e)))?;
64
65 let path = "m/44'/60'/0'/0/0";
67 let derived = master
68 .derive_path(path)
69 .map_err(|e| Error::Crypto(format!("BIP-44 derivation failed: {}", e)))?;
70
71 let derived_signing_key: &k256::ecdsa::SigningKey = derived.as_ref();
73 let mut private_key = [0u8; 32];
74 private_key.copy_from_slice(&derived_signing_key.to_bytes());
75
76 let signing_key = SigningKey::from_bytes((&private_key).into())
78 .map_err(|e| Error::Crypto(format!("Invalid private key: {}", e)))?;
79 let verifying_key = signing_key.verifying_key();
80 let public_key_bytes = verifying_key.to_encoded_point(false);
81 let pubkey_raw = &public_key_bytes.as_bytes()[1..]; let mut keccak = Keccak::v256();
86 let mut hash = [0u8; 32];
87 keccak.update(pubkey_raw);
88 keccak.finalize(&mut hash);
89
90 let address = format!("0x{}", hex::encode(&hash[12..]));
92
93 Ok(EthWallet {
94 private_key,
95 address,
96 })
97}
98
99pub fn derive_eoa_address(mnemonic: &str) -> Result<String> {
101 Ok(derive_eoa(mnemonic)?.address)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
111 fn test_eoa_derivation_parity() {
112 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
113 let w = derive_eoa(mnemonic).unwrap();
114 assert_eq!(
115 hex::encode(&w.private_key),
116 "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727",
117 "Private key must match reference derivation"
118 );
119 assert_eq!(
120 w.address.to_lowercase(),
121 "0x9858effd232b4033e47d90003d41ec34ecaeda94",
122 "EOA must match eth_account.from_mnemonic for the 12-word test mnemonic"
123 );
124 }
125
126 #[test]
127 fn test_derive_eoa_address() {
128 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
129 let address = derive_eoa_address(mnemonic).unwrap();
130 assert_eq!(
131 address.to_lowercase(),
132 "0x9858effd232b4033e47d90003d41ec34ecaeda94"
133 );
134 }
135
136 #[test]
137 fn test_ethwallet_serialization() {
138 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
139 let w = derive_eoa(mnemonic).unwrap();
140 let json = serde_json::to_string(&w).unwrap();
141 assert!(json.contains("1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"));
142 assert!(json.contains("0x9858effd232b4033e47d90003d41ec34ecaeda94"));
143
144 let w2: EthWallet = serde_json::from_str(&json).unwrap();
146 assert_eq!(w.private_key, w2.private_key);
147 assert_eq!(w.address, w2.address);
148 }
149}