metaflux_client/wallet/
key.rs1use core::fmt;
11
12use k256::ecdsa::SigningKey;
13use serde::{Deserialize, Serialize};
14use tiny_keccak::{Hasher, Keccak};
15
16use crate::error::ClientError;
17
18#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct Address(#[serde(with = "hex_array_20")] pub [u8; 20]);
25
26impl Address {
27 pub const ZERO: Self = Self([0u8; 20]);
29
30 #[must_use]
32 pub const fn from_bytes(bytes: [u8; 20]) -> Self {
33 Self(bytes)
34 }
35
36 pub fn from_hex(s: &str) -> Result<Self, ClientError> {
42 let stripped = s.strip_prefix("0x").unwrap_or(s);
43 if stripped.len() != 40 {
44 return Err(ClientError::InvalidKey(format!(
45 "address hex must be 40 chars, got {}",
46 stripped.len()
47 )));
48 }
49 let bytes = hex::decode(stripped)
50 .map_err(|e| ClientError::InvalidKey(format!("address hex decode: {e}")))?;
51 let mut out = [0u8; 20];
52 out.copy_from_slice(&bytes);
53 Ok(Self(out))
54 }
55
56 #[must_use]
58 pub const fn as_bytes(&self) -> &[u8; 20] {
59 &self.0
60 }
61}
62
63impl fmt::Debug for Address {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 write!(f, "0x{}", hex::encode(self.0))
66 }
67}
68
69impl fmt::Display for Address {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "0x{}", hex::encode(self.0))
72 }
73}
74
75mod hex_array_20 {
77 use serde::{Deserialize, Deserializer, Serializer};
78
79 pub fn serialize<S: Serializer>(bytes: &[u8; 20], s: S) -> Result<S::Ok, S::Error> {
80 let hex_str = format!("0x{}", hex::encode(bytes));
81 s.serialize_str(&hex_str)
82 }
83
84 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 20], D::Error> {
85 let s: String = String::deserialize(d)?;
86 let stripped = s.strip_prefix("0x").unwrap_or(&s);
87 if stripped.len() != 40 {
88 return Err(serde::de::Error::custom(format!(
89 "address hex must be 40 chars, got {}",
90 stripped.len()
91 )));
92 }
93 let bytes = hex::decode(stripped).map_err(serde::de::Error::custom)?;
94 let mut out = [0u8; 20];
95 out.copy_from_slice(&bytes);
96 Ok(out)
97 }
98}
99
100#[derive(Clone)]
106pub struct Wallet {
107 key: SigningKey,
108 address: Address,
109}
110
111impl Wallet {
112 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, ClientError> {
118 let key = SigningKey::from_bytes(&bytes.into())
119 .map_err(|e| ClientError::InvalidKey(e.to_string()))?;
120 let address = address_from_signing_key(&key);
121 Ok(Self { key, address })
122 }
123
124 pub fn from_hex(s: &str) -> Result<Self, ClientError> {
130 let stripped = s.strip_prefix("0x").unwrap_or(s);
131 if stripped.len() != 64 {
132 return Err(ClientError::InvalidKey(format!(
133 "private key hex must be 64 chars, got {}",
134 stripped.len()
135 )));
136 }
137 let raw = hex::decode(stripped)
138 .map_err(|e| ClientError::InvalidKey(format!("private key hex decode: {e}")))?;
139 let mut bytes = [0u8; 32];
140 bytes.copy_from_slice(&raw);
141 Self::from_bytes(bytes)
142 }
143
144 #[doc(hidden)]
150 #[must_use]
151 pub fn random_for_testing() -> Self {
152 let key = SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
153 let address = address_from_signing_key(&key);
154 Self { key, address }
155 }
156
157 #[must_use]
159 pub const fn address(&self) -> Address {
160 self.address
161 }
162
163 pub(crate) const fn signing_key(&self) -> &SigningKey {
167 &self.key
168 }
169}
170
171impl fmt::Debug for Wallet {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("Wallet")
174 .field("address", &self.address)
175 .field("key", &"<redacted>")
176 .finish()
177 }
178}
179
180fn address_from_signing_key(key: &SigningKey) -> Address {
186 let verifying = key.verifying_key();
187 let point = verifying.to_encoded_point(false);
188 let pubkey_bytes = point.as_bytes(); debug_assert_eq!(pubkey_bytes.len(), 65);
190 debug_assert_eq!(pubkey_bytes[0], 0x04);
191
192 let mut hasher = Keccak::v256();
193 hasher.update(&pubkey_bytes[1..]); let mut digest = [0u8; 32];
195 hasher.finalize(&mut digest);
196
197 let mut addr = [0u8; 20];
198 addr.copy_from_slice(&digest[12..32]);
199 Address(addr)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
211 fn known_vector_eip55_book_example() {
212 let priv_hex = "4646464646464646464646464646464646464646464646464646464646464646";
214 let wallet = Wallet::from_hex(priv_hex).unwrap();
215 let got = hex::encode(wallet.address().as_bytes());
216 assert_eq!(got, "9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f");
221 }
222
223 #[test]
224 fn from_hex_accepts_0x_prefix() {
225 let a =
226 Wallet::from_hex("0x4646464646464646464646464646464646464646464646464646464646464646")
227 .unwrap();
228 let b =
229 Wallet::from_hex("4646464646464646464646464646464646464646464646464646464646464646")
230 .unwrap();
231 assert_eq!(a.address(), b.address());
232 }
233
234 #[test]
235 fn from_hex_rejects_short_input() {
236 let e = Wallet::from_hex("dead").unwrap_err();
237 assert!(matches!(e, ClientError::InvalidKey(_)));
238 }
239
240 #[test]
241 fn from_hex_rejects_non_hex() {
242 let e = Wallet::from_hex(&"z".repeat(64)).unwrap_err();
243 assert!(matches!(e, ClientError::InvalidKey(_)));
244 }
245
246 #[test]
247 fn address_parses_with_and_without_prefix() {
248 let a = Address::from_hex("0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f").unwrap();
249 let b = Address::from_hex("9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f").unwrap();
250 assert_eq!(a, b);
251 }
252
253 #[test]
254 fn address_display_lowercase_hex() {
255 let a = Address::from_bytes([0x9du8; 20]);
256 let s = format!("{a}");
257 assert!(s.starts_with("0x"));
258 assert_eq!(s.len(), 42);
259 }
260
261 #[test]
262 fn debug_redacts_secret() {
263 let w = Wallet::random_for_testing();
264 let dbg = format!("{w:?}");
265 assert!(dbg.contains("redacted"));
266 assert!(!dbg.to_lowercase().contains("signingkey"));
267 }
268}