Skip to main content

kobe_sol/
deriver.rs

1//! Solana address derivation from HD wallet.
2
3use alloc::string::String;
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_core::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_core::Wallet`] and derives
29/// Solana addresses following BIP44/SLIP-0010 standards.
30///
31/// # Example
32///
33/// ```
34/// use kobe_core::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#[derive(Debug)]
43pub struct Deriver<'a> {
44    /// Reference to the wallet for seed access.
45    wallet: &'a Wallet,
46}
47
48impl<'a> Deriver<'a> {
49    /// Create a new Solana deriver from a wallet.
50    #[inline]
51    #[must_use]
52    pub const fn new(wallet: &'a Wallet) -> Self {
53        Self { wallet }
54    }
55
56    /// Derive a Solana address using the default Standard style.
57    ///
58    /// Uses path: `m/44'/501'/index'/0'` (Phantom, Backpack, etc.)
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if derivation fails.
63    #[inline]
64    pub fn derive(&self, index: u32) -> Result<DerivedAddress, Error> {
65        self.derive_with_style(DerivationStyle::Standard, index)
66    }
67
68    /// Derive a Solana address with a specific derivation style.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if derivation fails.
73    #[allow(deprecated)]
74    pub fn derive_with_style(
75        &self,
76        style: DerivationStyle,
77        index: u32,
78    ) -> Result<DerivedAddress, Error> {
79        let derived = match style {
80            DerivationStyle::Standard => {
81                DerivedKey::derive_standard_path(self.wallet.seed(), index)?
82            }
83            DerivationStyle::Trust => DerivedKey::derive_trust_path(self.wallet.seed(), index)?,
84            DerivationStyle::LedgerLive => {
85                DerivedKey::derive_ledger_live_path(self.wallet.seed(), index)?
86            }
87            DerivationStyle::Legacy => DerivedKey::derive_legacy_path(self.wallet.seed(), index)?,
88        };
89
90        let signing_key = derived.to_signing_key();
91        let verifying_key: VerifyingKey = signing_key.verifying_key();
92        let public_key_bytes = verifying_key.as_bytes();
93
94        Ok(DerivedAddress {
95            path: style.path(index),
96            private_key_hex: Zeroizing::new(hex::encode(derived.private_key.as_slice())),
97            public_key_hex: hex::encode(public_key_bytes),
98            address: bs58::encode(public_key_bytes).into_string(),
99        })
100    }
101
102    /// Derive multiple addresses using the default Standard style.
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if any derivation fails.
107    pub fn derive_many(&self, start_index: u32, count: u32) -> Result<Vec<DerivedAddress>, Error> {
108        self.derive_many_with_style(DerivationStyle::Standard, start_index, count)
109    }
110
111    /// Derive multiple addresses with a specific derivation style.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if any derivation fails.
116    pub fn derive_many_with_style(
117        &self,
118        style: DerivationStyle,
119        start_index: u32,
120        count: u32,
121    ) -> Result<Vec<DerivedAddress>, Error> {
122        (start_index..start_index + count)
123            .map(|account| self.derive_with_style(style, account))
124            .collect()
125    }
126}
127
128#[cfg(test)]
129#[allow(deprecated)]
130mod tests {
131    use super::*;
132
133    fn test_wallet() -> Wallet {
134        Wallet::from_mnemonic(
135            "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
136            None,
137        )
138        .unwrap()
139    }
140
141    #[test]
142    fn test_derive_address() {
143        let wallet = test_wallet();
144        let deriver = Deriver::new(&wallet);
145        let addr = deriver.derive(0).unwrap();
146
147        // Solana addresses are 32-44 characters in Base58
148        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
149        assert_eq!(addr.path, "m/44'/501'/0'/0'");
150    }
151
152    #[test]
153    fn test_derive_many() {
154        let wallet = test_wallet();
155        let deriver = Deriver::new(&wallet);
156        let addresses = deriver.derive_many(0, 3).unwrap();
157
158        assert_eq!(addresses.len(), 3);
159        assert_eq!(addresses[0].path, "m/44'/501'/0'/0'");
160        assert_eq!(addresses[1].path, "m/44'/501'/1'/0'");
161        assert_eq!(addresses[2].path, "m/44'/501'/2'/0'");
162
163        // All addresses should be unique
164        assert_ne!(addresses[0].address, addresses[1].address);
165        assert_ne!(addresses[1].address, addresses[2].address);
166    }
167
168    #[test]
169    fn test_deterministic_derivation() {
170        let wallet = test_wallet();
171        let deriver = Deriver::new(&wallet);
172
173        let addr1 = deriver.derive(0).unwrap();
174        let addr2 = deriver.derive(0).unwrap();
175
176        assert_eq!(addr1.address, addr2.address);
177        assert_eq!(*addr1.private_key_hex, *addr2.private_key_hex);
178    }
179
180    #[test]
181    fn test_derive_with_trust_style() {
182        let wallet = test_wallet();
183        let deriver = Deriver::new(&wallet);
184        let addr = deriver
185            .derive_with_style(DerivationStyle::Trust, 0)
186            .unwrap();
187
188        assert_eq!(addr.path, "m/44'/501'/0'");
189        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
190    }
191
192    #[test]
193    fn test_derive_with_ledger_live_style() {
194        let wallet = test_wallet();
195        let deriver = Deriver::new(&wallet);
196        let addr = deriver
197            .derive_with_style(DerivationStyle::LedgerLive, 0)
198            .unwrap();
199
200        assert_eq!(addr.path, "m/44'/501'/0'/0'/0'");
201        assert!(addr.address.len() >= 32 && addr.address.len() <= 44);
202    }
203
204    #[test]
205    fn test_different_styles_produce_different_addresses() {
206        let wallet = test_wallet();
207        let deriver = Deriver::new(&wallet);
208
209        let standard = deriver
210            .derive_with_style(DerivationStyle::Standard, 0)
211            .unwrap();
212        let trust = deriver
213            .derive_with_style(DerivationStyle::Trust, 0)
214            .unwrap();
215        let ledger_live = deriver
216            .derive_with_style(DerivationStyle::LedgerLive, 0)
217            .unwrap();
218        let legacy = deriver
219            .derive_with_style(DerivationStyle::Legacy, 0)
220            .unwrap();
221
222        // All styles should produce different addresses
223        assert_ne!(standard.address, trust.address);
224        assert_ne!(standard.address, ledger_live.address);
225        assert_ne!(standard.address, legacy.address);
226        assert_ne!(trust.address, ledger_live.address);
227    }
228}