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    /// Import a wallet from a private key hex string.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the hex string is invalid or the private key is invalid.
65    pub fn from_private_key_hex(hex_str: &str) -> Result<Self, Error> {
66        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
67        let bytes = hex::decode(hex_str).map_err(|_| Error::InvalidHex)?;
68
69        let private_key = SigningKey::from_slice(&bytes).map_err(|_| Error::InvalidPrivateKey)?;
70        let address = Self::derive_address(&private_key);
71
72        Ok(Self {
73            private_key,
74            address,
75        })
76    }
77
78    /// Derive address from private key.
79    fn derive_address(private_key: &SigningKey) -> Address {
80        let public_key = private_key.verifying_key();
81        let public_key_bytes = public_key.to_encoded_point(false);
82        public_key_to_address(public_key_bytes.as_bytes())
83    }
84
85    /// Get the private key in hex format without 0x prefix (zeroized on drop).
86    #[inline]
87    #[must_use]
88    pub fn private_key_hex(&self) -> Zeroizing<String> {
89        Zeroizing::new(hex::encode(self.private_key.to_bytes()))
90    }
91
92    /// Get the public key in uncompressed hex format without 0x prefix.
93    #[inline]
94    #[must_use]
95    pub fn public_key_hex(&self) -> String {
96        let public_key = self.private_key.verifying_key();
97        let bytes = public_key.to_encoded_point(false);
98        hex::encode(bytes.as_bytes())
99    }
100
101    /// Get the Ethereum address.
102    #[must_use]
103    pub const fn address(&self) -> &Address {
104        &self.address
105    }
106
107    /// Get the checksummed address string.
108    #[inline]
109    #[must_use]
110    pub fn address_string(&self) -> String {
111        to_checksum_address(&self.address)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[cfg(feature = "rand")]
120    #[test]
121    fn test_generate() {
122        let wallet = StandardWallet::generate().unwrap();
123        assert!(wallet.address_string().starts_with("0x"));
124        assert_eq!(wallet.address_string().len(), 42);
125    }
126
127    #[cfg(feature = "rand")]
128    #[test]
129    fn test_from_private_key() {
130        let wallet = StandardWallet::generate().unwrap();
131        let pk_hex = wallet.private_key_hex();
132
133        let imported = StandardWallet::from_private_key_hex(&pk_hex).unwrap();
134        assert_eq!(wallet.address_string(), imported.address_string());
135    }
136
137    #[cfg(feature = "rand")]
138    #[test]
139    fn test_from_private_key_with_prefix() {
140        use alloc::format;
141        let wallet = StandardWallet::generate().unwrap();
142        let pk_hex = format!("0x{}", wallet.private_key_hex().as_str());
143
144        let imported = StandardWallet::from_private_key_hex(&pk_hex).unwrap();
145        assert_eq!(wallet.address_string(), imported.address_string());
146    }
147}