Skip to main content

kobe_svm/
deriver.rs

1//! Solana address derivation from HD wallet.
2
3use alloc::string::{String, ToString};
4use alloc::vec::Vec;
5
6use ed25519_dalek::VerifyingKey;
7use kobe::Wallet;
8use zeroize::Zeroizing;
9
10use crate::Error;
11use crate::derivation_style::DerivationStyle;
12use crate::slip10::DerivedKey;
13
14/// A derived Solana address with associated keys.
15#[derive(Debug, Clone)]
16pub struct DerivedAddress {
17    /// Derivation path used (e.g., `m/44'/501'/0'/0'`).
18    pub path: String,
19    /// Private key in hex format (zeroized on drop).
20    pub private_key_hex: Zeroizing<String>,
21    /// Full keypair in base58 format (64 bytes: secret 32B + public 32B, zeroized on drop).
22    ///
23    /// This is the standard format used by Phantom, Backpack, Solflare wallets.
24    pub keypair_base58: Zeroizing<String>,
25    /// Public key in hex format.
26    pub public_key_hex: String,
27    /// Solana address (Base58 encoded public key).
28    pub address: String,
29}
30
31/// Solana address deriver from a unified wallet seed.
32///
33/// This deriver takes a seed from [`kobe::Wallet`] and derives
34/// Solana addresses following BIP44/SLIP-0010 standards.
35#[derive(Debug)]
36pub struct Deriver<'a> {
37    /// Reference to the wallet for seed access.
38    wallet: &'a Wallet,
39}
40
41impl<'a> Deriver<'a> {
42    /// Create a new Solana deriver from a wallet.
43    #[inline]
44    #[must_use]
45    pub const fn new(wallet: &'a Wallet) -> Self {
46        Self { wallet }
47    }
48
49    /// Derive a Solana address using the Standard derivation style.
50    ///
51    /// Uses path: `m/44'/501'/index'/0'` (Phantom, Backpack, etc.)
52    ///
53    /// # Arguments
54    ///
55    /// * `index` - The address index
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if derivation fails.
60    #[inline]
61    pub fn derive(&self, index: u32) -> Result<DerivedAddress, Error> {
62        self.derive_with(DerivationStyle::Standard, index)
63    }
64
65    /// Derive a Solana address with a specific derivation style.
66    ///
67    /// This method supports different wallet path formats:
68    /// - **Standard** (Phantom/Backpack): `m/44'/501'/index'/0'`
69    /// - **Trust**: `m/44'/501'/index'`
70    /// - **Ledger Live**: `m/44'/501'/index'/0'/0'`
71    /// - **Legacy**: `m/44'/501'/0'/index'`
72    ///
73    /// # Arguments
74    ///
75    /// * `style` - The derivation style to use
76    /// * `index` - The address/account index
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if derivation fails.
81    #[allow(deprecated)]
82    pub fn derive_with(&self, style: DerivationStyle, index: u32) -> Result<DerivedAddress, Error> {
83        let derived = match style {
84            DerivationStyle::Standard => {
85                DerivedKey::derive_standard_path(self.wallet.seed(), index)?
86            }
87            DerivationStyle::Trust => DerivedKey::derive_trust_path(self.wallet.seed(), index)?,
88            DerivationStyle::LedgerLive => {
89                DerivedKey::derive_ledger_live_path(self.wallet.seed(), index)?
90            }
91            DerivationStyle::Legacy => DerivedKey::derive_legacy_path(self.wallet.seed(), index)?,
92        };
93
94        let signing_key = derived.to_signing_key();
95        let verifying_key: VerifyingKey = signing_key.verifying_key();
96        let public_key_bytes = verifying_key.as_bytes();
97
98        // Build base58-encoded 64-byte keypair (secret 32B + public 32B)
99        let mut keypair_bytes = [0u8; 64];
100        keypair_bytes[..32].copy_from_slice(derived.private_key.as_slice());
101        keypair_bytes[32..].copy_from_slice(public_key_bytes);
102        let keypair_b58 = bs58::encode(&keypair_bytes).into_string();
103        keypair_bytes.fill(0);
104
105        Ok(DerivedAddress {
106            path: style.path(index),
107            private_key_hex: Zeroizing::new(hex::encode(derived.private_key.as_slice())),
108            keypair_base58: Zeroizing::new(keypair_b58),
109            public_key_hex: hex::encode(public_key_bytes),
110            address: bs58::encode(public_key_bytes).into_string(),
111        })
112    }
113
114    /// Derive multiple addresses using the Standard derivation style.
115    ///
116    /// # Arguments
117    ///
118    /// * `start` - Starting address index
119    /// * `count` - Number of addresses to derive
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if any derivation fails.
124    #[inline]
125    pub fn derive_many(&self, start: u32, count: u32) -> Result<Vec<DerivedAddress>, Error> {
126        self.derive_many_with(DerivationStyle::Standard, start, count)
127    }
128
129    /// Derive multiple addresses with a specific derivation style.
130    ///
131    /// # Arguments
132    ///
133    /// * `style` - The derivation style to use
134    /// * `start` - Starting index
135    /// * `count` - Number of addresses to derive
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if any derivation fails.
140    pub fn derive_many_with(
141        &self,
142        style: DerivationStyle,
143        start: u32,
144        count: u32,
145    ) -> Result<Vec<DerivedAddress>, Error> {
146        (start..start + count)
147            .map(|index| self.derive_with(style, index))
148            .collect()
149    }
150
151    /// Derive an address at a custom derivation path.
152    ///
153    /// This is the lowest-level derivation method, allowing full control
154    /// over the derivation path.
155    ///
156    /// **Note**: Ed25519 (Solana) only supports hardened derivation.
157    /// All path components will be treated as hardened.
158    ///
159    /// # Arguments
160    ///
161    /// * `path` - SLIP-0010 derivation path (e.g., `m/44'/501'/0'/0'`)
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if derivation fails.
166    pub fn derive_path(&self, path: &str) -> Result<DerivedAddress, Error> {
167        let derived = DerivedKey::derive_path(self.wallet.seed(), path)?;
168
169        let signing_key = derived.to_signing_key();
170        let verifying_key: VerifyingKey = signing_key.verifying_key();
171        let public_key_bytes = verifying_key.as_bytes();
172
173        // Build base58-encoded 64-byte keypair (secret 32B + public 32B)
174        let mut keypair_bytes = [0u8; 64];
175        keypair_bytes[..32].copy_from_slice(derived.private_key.as_slice());
176        keypair_bytes[32..].copy_from_slice(public_key_bytes);
177        let keypair_b58 = bs58::encode(&keypair_bytes).into_string();
178        keypair_bytes.fill(0);
179
180        Ok(DerivedAddress {
181            path: path.to_string(),
182            private_key_hex: Zeroizing::new(hex::encode(derived.private_key.as_slice())),
183            keypair_base58: Zeroizing::new(keypair_b58),
184            public_key_hex: hex::encode(public_key_bytes),
185            address: bs58::encode(public_key_bytes).into_string(),
186        })
187    }
188}
189
190#[cfg(test)]
191#[allow(deprecated)]
192mod tests {
193    use super::*;
194
195    fn test_wallet() -> Wallet {
196        Wallet::from_mnemonic(
197            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
198            None,
199        )
200        .unwrap()
201    }
202
203    #[test]
204    fn test_derive_address() {
205        let wallet = test_wallet();
206        let deriver = Deriver::new(&wallet);
207        let addr = deriver.derive(0).unwrap();
208
209        // Solana addresses are 32-44 characters in Base58
210        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
211        assert_eq!(addr.path, "m/44'/501'/0'/0'");
212    }
213
214    #[test]
215    fn test_derive_many() {
216        let wallet = test_wallet();
217        let deriver = Deriver::new(&wallet);
218        let addresses = deriver.derive_many(0, 3).unwrap();
219
220        assert_eq!(addresses.len(), 3);
221        assert_eq!(addresses[0].path, "m/44'/501'/0'/0'");
222        assert_eq!(addresses[1].path, "m/44'/501'/1'/0'");
223        assert_eq!(addresses[2].path, "m/44'/501'/2'/0'");
224
225        // All addresses should be unique
226        assert_ne!(addresses[0].address, addresses[1].address);
227        assert_ne!(addresses[1].address, addresses[2].address);
228    }
229
230    #[test]
231    fn test_deterministic_derivation() {
232        let wallet = test_wallet();
233        let deriver = Deriver::new(&wallet);
234
235        let addr1 = deriver.derive(0).unwrap();
236        let addr2 = deriver.derive(0).unwrap();
237
238        assert_eq!(addr1.address, addr2.address);
239        assert_eq!(*addr1.private_key_hex, *addr2.private_key_hex);
240    }
241
242    #[test]
243    fn test_derive_with_trust() {
244        let wallet = test_wallet();
245        let deriver = Deriver::new(&wallet);
246        let addr = deriver.derive_with(DerivationStyle::Trust, 0).unwrap();
247
248        assert_eq!(addr.path, "m/44'/501'/0'");
249        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
250    }
251
252    #[test]
253    fn test_derive_with_ledger_live() {
254        let wallet = test_wallet();
255        let deriver = Deriver::new(&wallet);
256        let addr = deriver.derive_with(DerivationStyle::LedgerLive, 0).unwrap();
257
258        assert_eq!(addr.path, "m/44'/501'/0'/0'/0'");
259        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
260    }
261
262    #[test]
263    fn test_different_styles_produce_different_addresses() {
264        let wallet = test_wallet();
265        let deriver = Deriver::new(&wallet);
266
267        let standard = deriver.derive_with(DerivationStyle::Standard, 0).unwrap();
268        let trust = deriver.derive_with(DerivationStyle::Trust, 0).unwrap();
269        let ledger_live = deriver.derive_with(DerivationStyle::LedgerLive, 0).unwrap();
270        let legacy = deriver.derive_with(DerivationStyle::Legacy, 0).unwrap();
271
272        // All styles should produce different addresses
273        assert_ne!(standard.address, trust.address);
274        assert_ne!(standard.address, ledger_live.address);
275        assert_ne!(standard.address, legacy.address);
276        assert_ne!(trust.address, ledger_live.address);
277    }
278}