Skip to main content

kobe_evm/
standard_wallet.rs

1//! Standard (non-HD) Ethereum wallet implementation.
2//!
3//! A standard wallet uses a single randomly generated private key,
4//! without mnemonic or HD derivation.
5
6#[cfg(feature = "alloc")]
7use alloc::string::String;
8
9use alloy_primitives::Address;
10use k256::ecdsa::SigningKey;
11use zeroize::{Zeroize, Zeroizing};
12
13use crate::Error;
14use crate::address::{public_key_to_address, to_checksum_address};
15
16/// A standard Ethereum wallet with a single private key.
17///
18/// This wallet type generates a random private key directly,
19/// without using a mnemonic or HD derivation.
20#[derive(Debug)]
21pub struct StandardWallet {
22    /// ECDSA signing key (secp256k1).
23    private_key: SigningKey,
24    /// Ethereum address derived from public key.
25    address: Address,
26}
27
28impl StandardWallet {
29    /// Generate a new standard wallet with a random private key.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if key generation fails.
34    ///
35    /// # Note
36    ///
37    /// This function requires the `rand` feature to be enabled.
38    #[cfg(feature = "rand")]
39    pub fn generate() -> Result<Self, Error> {
40        use k256::elliptic_curve::rand_core::OsRng;
41        let private_key = SigningKey::random(&mut OsRng);
42        let address = Self::derive_address(&private_key)?;
43
44        Ok(Self {
45            private_key,
46            address,
47        })
48    }
49
50    /// Create a wallet from raw 32-byte secret key.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the secret key is invalid.
55    pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, Error> {
56        let private_key = SigningKey::from_slice(bytes).map_err(|_| Error::InvalidPrivateKey)?;
57        let address = Self::derive_address(&private_key)?;
58
59        Ok(Self {
60            private_key,
61            address,
62        })
63    }
64
65    /// Import a wallet from a hex-encoded secret key.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if the hex string is invalid or the secret key is invalid.
70    pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
71        let stripped = hex_str.strip_prefix("0x").unwrap_or(hex_str);
72        let mut bytes = hex::decode(stripped).map_err(|_| Error::InvalidHex)?;
73
74        let result = SigningKey::from_slice(&bytes).map_err(|_| Error::InvalidPrivateKey);
75        bytes.zeroize();
76        let private_key = result?;
77        let address = Self::derive_address(&private_key)?;
78
79        Ok(Self {
80            private_key,
81            address,
82        })
83    }
84
85    /// Derive address from private key.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the public key conversion fails.
90    fn derive_address(private_key: &SigningKey) -> Result<Address, Error> {
91        let public_key = private_key.verifying_key();
92        let public_key_bytes = public_key.to_encoded_point(false);
93        public_key_to_address(public_key_bytes.as_bytes())
94    }
95
96    /// Get the secret key as raw bytes (zeroized on drop).
97    #[inline]
98    #[must_use]
99    pub fn secret_bytes(&self) -> Zeroizing<[u8; 32]> {
100        Zeroizing::new(self.private_key.to_bytes().into())
101    }
102
103    /// Get the secret key in hex format without 0x prefix (zeroized on drop).
104    #[inline]
105    #[must_use]
106    pub fn secret_hex(&self) -> Zeroizing<String> {
107        Zeroizing::new(hex::encode(self.private_key.to_bytes()))
108    }
109
110    /// Get the public key in uncompressed hex format without 0x prefix.
111    #[inline]
112    #[must_use]
113    pub fn pubkey_hex(&self) -> String {
114        let public_key = self.private_key.verifying_key();
115        let bytes = public_key.to_encoded_point(false);
116        hex::encode(bytes.as_bytes())
117    }
118
119    /// Get the checksummed Ethereum address string.
120    #[inline]
121    #[must_use]
122    pub fn address(&self) -> String {
123        to_checksum_address(&self.address)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[cfg(feature = "rand")]
132    #[test]
133    fn test_generate() {
134        let wallet = StandardWallet::generate().unwrap();
135        assert!(wallet.address().starts_with("0x"));
136        assert_eq!(wallet.address().len(), 42);
137    }
138
139    #[cfg(feature = "rand")]
140    #[test]
141    fn test_from_hex() {
142        let wallet = StandardWallet::generate().unwrap();
143        let hex = wallet.secret_hex();
144
145        let imported = StandardWallet::from_hex(&hex).unwrap();
146        assert_eq!(wallet.address(), imported.address());
147    }
148
149    #[cfg(feature = "rand")]
150    #[test]
151    fn test_from_hex_with_prefix() {
152        use alloc::format;
153        let wallet = StandardWallet::generate().unwrap();
154        let hex = format!("0x{}", wallet.secret_hex().as_str());
155
156        let imported = StandardWallet::from_hex(&hex).unwrap();
157        assert_eq!(wallet.address(), imported.address());
158    }
159
160    #[cfg(feature = "rand")]
161    #[test]
162    fn test_from_bytes() {
163        let wallet = StandardWallet::generate().unwrap();
164        let bytes = wallet.secret_bytes();
165
166        let imported = StandardWallet::from_bytes(&bytes).unwrap();
167        assert_eq!(wallet.address(), imported.address());
168    }
169}