Skip to main content

kobe_sol/
standard_wallet.rs

1//! Standard (non-HD) Solana wallet implementation.
2//!
3//! A standard wallet uses a single randomly generated private key,
4//! without mnemonic or HD derivation.
5
6use alloc::string::String;
7use ed25519_dalek::{SigningKey, VerifyingKey};
8use zeroize::Zeroizing;
9
10use crate::Error;
11
12/// A standard Solana wallet with a single keypair.
13///
14/// This wallet type generates a random private key directly,
15/// without using a mnemonic or HD derivation.
16///
17/// # Example
18///
19/// ```ignore
20/// use kobe_sol::StandardWallet;
21///
22/// let wallet = StandardWallet::generate().unwrap();
23/// println!("Address: {}", wallet.address_string());
24/// println!("Private Key: {}", wallet.private_key_hex().as_str());
25/// ```
26#[derive(Debug)]
27pub struct StandardWallet {
28    /// Ed25519 signing key.
29    signing_key: SigningKey,
30}
31
32impl StandardWallet {
33    /// Generate a new random wallet.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error if random generation fails.
38    #[cfg(feature = "rand")]
39    pub fn generate() -> Result<Self, Error> {
40        let mut bytes = [0u8; 32];
41        getrandom::fill(&mut bytes)
42            .map_err(|e| Error::Derivation(alloc::format!("random generation failed: {e}")))?;
43        let signing_key = SigningKey::from_bytes(&bytes);
44        // Zeroize the temporary buffer
45        bytes.fill(0);
46        Ok(Self { signing_key })
47    }
48
49    /// Create a wallet from raw 32-byte secret key.
50    #[must_use]
51    pub fn from_bytes(bytes: &[u8; 32]) -> Self {
52        let signing_key = SigningKey::from_bytes(bytes);
53        Self { signing_key }
54    }
55
56    /// Create a wallet from a hex-encoded secret key.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if the hex is invalid or key length is wrong.
61    pub fn from_hex(hex_key: &str) -> Result<Self, Error> {
62        let bytes = hex::decode(hex_key).map_err(|_| Error::InvalidHex)?;
63
64        if bytes.len() != 32 {
65            return Err(Error::Derivation(alloc::format!(
66                "expected 32 bytes, got {}",
67                bytes.len()
68            )));
69        }
70
71        let mut key_bytes = [0u8; 32];
72        key_bytes.copy_from_slice(&bytes);
73        Ok(Self::from_bytes(&key_bytes))
74    }
75
76    /// Get the Solana address as Base58 encoded string.
77    #[inline]
78    #[must_use]
79    pub fn address(&self) -> String {
80        let verifying_key: VerifyingKey = self.signing_key.verifying_key();
81        bs58::encode(verifying_key.as_bytes()).into_string()
82    }
83
84    /// Get the secret key as raw bytes (zeroized on drop).
85    #[inline]
86    #[must_use]
87    pub fn secret_bytes(&self) -> Zeroizing<[u8; 32]> {
88        Zeroizing::new(*self.signing_key.as_bytes())
89    }
90
91    /// Get the secret key in hex format (zeroized on drop).
92    #[inline]
93    #[must_use]
94    pub fn secret_hex(&self) -> Zeroizing<String> {
95        Zeroizing::new(hex::encode(self.signing_key.as_bytes()))
96    }
97
98    /// Get the public key in hex format.
99    #[inline]
100    #[must_use]
101    pub fn pubkey_hex(&self) -> String {
102        let verifying_key: VerifyingKey = self.signing_key.verifying_key();
103        hex::encode(verifying_key.as_bytes())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[cfg(feature = "rand")]
112    #[test]
113    fn test_generate() {
114        let wallet = StandardWallet::generate().unwrap();
115        let address = wallet.address();
116
117        // Solana addresses are 32-44 characters in Base58
118        assert!(address.len() >= 32 && address.len() <= 44);
119    }
120
121    #[test]
122    fn test_from_bytes() {
123        let key = [1u8; 32];
124        let wallet = StandardWallet::from_bytes(&key);
125        let address = wallet.address();
126
127        assert!(address.len() >= 32 && address.len() <= 44);
128    }
129
130    #[test]
131    fn test_from_hex() {
132        let hex_key = "0101010101010101010101010101010101010101010101010101010101010101";
133        let wallet = StandardWallet::from_hex(hex_key).unwrap();
134        let address = wallet.address();
135
136        assert!(address.len() >= 32 && address.len() <= 44);
137    }
138
139    #[test]
140    fn test_deterministic() {
141        let key = [42u8; 32];
142        let wallet1 = StandardWallet::from_bytes(&key);
143        let wallet2 = StandardWallet::from_bytes(&key);
144
145        assert_eq!(wallet1.address(), wallet2.address());
146    }
147}