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::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 hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
72        let bytes = hex::decode(hex_str).map_err(|_| Error::InvalidHex)?;
73
74        let private_key = SigningKey::from_slice(&bytes).map_err(|_| Error::InvalidPrivateKey)?;
75        let address = Self::derive_address(&private_key);
76
77        Ok(Self {
78            private_key,
79            address,
80        })
81    }
82
83    /// Derive address from private key.
84    fn derive_address(private_key: &SigningKey) -> Address {
85        let public_key = private_key.verifying_key();
86        let public_key_bytes = public_key.to_encoded_point(false);
87        public_key_to_address(public_key_bytes.as_bytes())
88    }
89
90    /// Get the secret key as raw bytes (zeroized on drop).
91    #[inline]
92    #[must_use]
93    pub fn secret_bytes(&self) -> Zeroizing<[u8; 32]> {
94        Zeroizing::new(self.private_key.to_bytes().into())
95    }
96
97    /// Get the secret key in hex format without 0x prefix (zeroized on drop).
98    #[inline]
99    #[must_use]
100    pub fn secret_hex(&self) -> Zeroizing<String> {
101        Zeroizing::new(hex::encode(self.private_key.to_bytes()))
102    }
103
104    /// Get the public key in uncompressed hex format without 0x prefix.
105    #[inline]
106    #[must_use]
107    pub fn pubkey_hex(&self) -> String {
108        let public_key = self.private_key.verifying_key();
109        let bytes = public_key.to_encoded_point(false);
110        hex::encode(bytes.as_bytes())
111    }
112
113    /// Get the checksummed Ethereum address string.
114    #[inline]
115    #[must_use]
116    pub fn address(&self) -> String {
117        to_checksum_address(&self.address)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[cfg(feature = "rand")]
126    #[test]
127    fn test_generate() {
128        let wallet = StandardWallet::generate().unwrap();
129        assert!(wallet.address().starts_with("0x"));
130        assert_eq!(wallet.address().len(), 42);
131    }
132
133    #[cfg(feature = "rand")]
134    #[test]
135    fn test_from_hex() {
136        let wallet = StandardWallet::generate().unwrap();
137        let hex = wallet.secret_hex();
138
139        let imported = StandardWallet::from_hex(&hex).unwrap();
140        assert_eq!(wallet.address(), imported.address());
141    }
142
143    #[cfg(feature = "rand")]
144    #[test]
145    fn test_from_hex_with_prefix() {
146        use alloc::format;
147        let wallet = StandardWallet::generate().unwrap();
148        let hex = format!("0x{}", wallet.secret_hex().as_str());
149
150        let imported = StandardWallet::from_hex(&hex).unwrap();
151        assert_eq!(wallet.address(), imported.address());
152    }
153
154    #[cfg(feature = "rand")]
155    #[test]
156    fn test_from_bytes() {
157        let wallet = StandardWallet::generate().unwrap();
158        let bytes = wallet.secret_bytes();
159
160        let imported = StandardWallet::from_bytes(&bytes).unwrap();
161        assert_eq!(wallet.address(), imported.address());
162    }
163}