Skip to main content

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