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