Skip to main content

kobe_sol/
deriver.rs

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