Skip to main content

kobe_sol/
standard_wallet.rs

1//! Standard single-key Solana wallet (no mnemonic).
2
3use alloc::string::String;
4use ed25519_dalek::{SigningKey, VerifyingKey};
5use zeroize::Zeroizing;
6
7use crate::Error;
8
9/// A standard Solana wallet with a single keypair (no HD derivation).
10#[derive(Debug)]
11pub struct StandardWallet {
12    signing_key: SigningKey,
13}
14
15impl StandardWallet {
16    /// Generate a new random wallet.
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if random generation fails.
21    #[cfg(feature = "rand")]
22    pub fn generate() -> Result<Self, Error> {
23        use rand_core::OsRng;
24        let signing_key = SigningKey::generate(&mut OsRng);
25        Ok(Self { signing_key })
26    }
27
28    /// Create a wallet from a raw 32-byte private key.
29    #[must_use]
30    pub fn from_private_key(private_key: &[u8; 32]) -> Self {
31        let signing_key = SigningKey::from_bytes(private_key);
32        Self { signing_key }
33    }
34
35    /// Create a wallet from a hex-encoded private key.
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the hex is invalid or key length is wrong.
40    pub fn from_private_key_hex(hex_key: &str) -> Result<Self, Error> {
41        let bytes = hex::decode(hex_key)
42            .map_err(|e| Error::Derivation(alloc::format!("invalid hex: {e}")))?;
43
44        if bytes.len() != 32 {
45            return Err(Error::Derivation(alloc::format!(
46                "expected 32 bytes, got {}",
47                bytes.len()
48            )));
49        }
50
51        let mut key_bytes = [0u8; 32];
52        key_bytes.copy_from_slice(&bytes);
53        Ok(Self::from_private_key(&key_bytes))
54    }
55
56    /// Get the Solana address (Base58 encoded public key).
57    #[inline]
58    #[must_use]
59    pub fn address_string(&self) -> String {
60        let verifying_key: VerifyingKey = self.signing_key.verifying_key();
61        bs58::encode(verifying_key.as_bytes()).into_string()
62    }
63
64    /// Get the private key as hex string.
65    #[inline]
66    #[must_use]
67    pub fn private_key_hex(&self) -> Zeroizing<String> {
68        Zeroizing::new(hex::encode(self.signing_key.as_bytes()))
69    }
70
71    /// Get the public key as hex string.
72    #[inline]
73    #[must_use]
74    pub fn public_key_hex(&self) -> String {
75        let verifying_key: VerifyingKey = self.signing_key.verifying_key();
76        hex::encode(verifying_key.as_bytes())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[cfg(feature = "rand")]
85    #[test]
86    fn test_generate() {
87        let wallet = StandardWallet::generate().unwrap();
88        let address = wallet.address_string();
89
90        // Solana addresses are 32-44 characters in Base58
91        assert!(address.len() >= 32 && address.len() <= 44);
92    }
93
94    #[test]
95    fn test_from_private_key() {
96        let key = [1u8; 32];
97        let wallet = StandardWallet::from_private_key(&key);
98        let address = wallet.address_string();
99
100        assert!(address.len() >= 32 && address.len() <= 44);
101    }
102
103    #[test]
104    fn test_from_private_key_hex() {
105        let hex_key = "0101010101010101010101010101010101010101010101010101010101010101";
106        let wallet = StandardWallet::from_private_key_hex(hex_key).unwrap();
107        let address = wallet.address_string();
108
109        assert!(address.len() >= 32 && address.len() <= 44);
110    }
111
112    #[test]
113    fn test_deterministic() {
114        let key = [42u8; 32];
115        let wallet1 = StandardWallet::from_private_key(&key);
116        let wallet2 = StandardWallet::from_private_key(&key);
117
118        assert_eq!(wallet1.address_string(), wallet2.address_string());
119    }
120}