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
5#[cfg(feature = "alloc")]
6use alloc::string::String;
7
8use alloy_primitives::Address;
9use k256::ecdsa::SigningKey;
10use zeroize::Zeroizing;
11
12use crate::Error;
13use crate::utils::{public_key_to_address, to_checksum_address};
14
15/// A standard Ethereum wallet with a single private key.
16///
17/// This wallet type generates a random private key directly,
18/// without using a mnemonic or HD derivation.
19#[derive(Debug)]
20pub struct StandardWallet {
21    /// Private key.
22    private_key: SigningKey,
23    /// Ethereum address.
24    address: Address,
25}
26
27impl StandardWallet {
28    /// Generate a new standard wallet with a random private key.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if key generation fails.
33    ///
34    /// # Note
35    ///
36    /// This function requires the `rand` feature to be enabled.
37    #[cfg(feature = "rand")]
38    pub fn generate() -> Result<Self, Error> {
39        use k256::elliptic_curve::rand_core::OsRng;
40        let private_key = SigningKey::random(&mut OsRng);
41        let address = Self::derive_address(&private_key);
42
43        Ok(Self {
44            private_key,
45            address,
46        })
47    }
48
49    /// Import a wallet from a private key hex string.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the private key is invalid.
54    pub fn from_private_key_hex(hex_str: &str) -> Result<Self, Error> {
55        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
56        let bytes = hex::decode(hex_str).map_err(|_| Error::InvalidPrivateKey)?;
57
58        let private_key = SigningKey::from_slice(&bytes).map_err(|_| Error::InvalidPrivateKey)?;
59        let address = Self::derive_address(&private_key);
60
61        Ok(Self {
62            private_key,
63            address,
64        })
65    }
66
67    /// Derive address from private key.
68    fn derive_address(private_key: &SigningKey) -> Address {
69        let public_key = private_key.verifying_key();
70        let public_key_bytes = public_key.to_encoded_point(false);
71        public_key_to_address(public_key_bytes.as_bytes())
72    }
73
74    /// Get the private key in hex format (without 0x prefix).
75    #[must_use]
76    pub fn private_key_hex(&self) -> Zeroizing<String> {
77        Zeroizing::new(hex::encode(self.private_key.to_bytes()))
78    }
79
80    /// Get the public key in hex format (uncompressed, without 0x prefix).
81    #[must_use]
82    pub fn public_key_hex(&self) -> String {
83        let public_key = self.private_key.verifying_key();
84        let bytes = public_key.to_encoded_point(false);
85        hex::encode(bytes.as_bytes())
86    }
87
88    /// Get the Ethereum address.
89    #[must_use]
90    pub const fn address(&self) -> &Address {
91        &self.address
92    }
93
94    /// Get the checksummed address string.
95    #[must_use]
96    pub fn address_string(&self) -> String {
97        to_checksum_address(&self.address)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[cfg(feature = "rand")]
106    #[test]
107    fn test_generate() {
108        let wallet = StandardWallet::generate().unwrap();
109        assert!(wallet.address_string().starts_with("0x"));
110        assert_eq!(wallet.address_string().len(), 42);
111    }
112
113    #[cfg(feature = "rand")]
114    #[test]
115    fn test_from_private_key() {
116        let wallet = StandardWallet::generate().unwrap();
117        let pk_hex = wallet.private_key_hex();
118
119        let imported = StandardWallet::from_private_key_hex(&pk_hex).unwrap();
120        assert_eq!(wallet.address_string(), imported.address_string());
121    }
122
123    #[cfg(feature = "rand")]
124    #[test]
125    fn test_from_private_key_with_prefix() {
126        use alloc::format;
127        let wallet = StandardWallet::generate().unwrap();
128        let pk_hex = format!("0x{}", wallet.private_key_hex().as_str());
129
130        let imported = StandardWallet::from_private_key_hex(&pk_hex).unwrap();
131        assert_eq!(wallet.address_string(), imported.address_string());
132    }
133}