Skip to main content

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