Skip to main content

kobe_eth/
deriver.rs

1//! Ethereum address derivation from a unified wallet.
2
3#[cfg(feature = "alloc")]
4use alloc::{
5    format,
6    string::{String, ToString},
7    vec::Vec,
8};
9
10use bip32::{DerivationPath, XPrv};
11use k256::ecdsa::SigningKey;
12use kobe_core::Wallet;
13use zeroize::Zeroizing;
14
15use crate::Error;
16use crate::address::{public_key_to_address, to_checksum_address};
17use crate::derivation_style::DerivationStyle;
18
19/// Ethereum address deriver from a unified wallet seed.
20///
21/// This deriver takes a seed from [`kobe_core::Wallet`] and derives
22/// Ethereum addresses following BIP32/44 standards.
23///
24/// # Example
25///
26/// ```
27/// use kobe_core::Wallet;
28/// use kobe_eth::Deriver;
29///
30/// let wallet = Wallet::generate(12, None).unwrap();
31/// let deriver = Deriver::new(&wallet);
32/// let addr = deriver.derive(0, false, 0).unwrap();
33/// println!("Address: {}", addr.address);
34/// ```
35#[derive(Debug)]
36pub struct Deriver<'a> {
37    /// Reference to the wallet for seed access.
38    wallet: &'a Wallet,
39}
40
41/// A derived Ethereum address with associated keys.
42#[derive(Debug, Clone)]
43pub struct DerivedAddress {
44    /// Derivation path used (e.g., `m/44'/60'/0'/0/0`).
45    pub path: String,
46    /// Private key in hex format without 0x prefix (zeroized on drop).
47    pub private_key_hex: Zeroizing<String>,
48    /// Public key in uncompressed hex format.
49    pub public_key_hex: String,
50    /// Checksummed Ethereum address (EIP-55)..
51    pub address: String,
52}
53
54impl<'a> Deriver<'a> {
55    /// Create a new Ethereum deriver from a wallet.
56    #[must_use]
57    pub const fn new(wallet: &'a Wallet) -> Self {
58        Self { wallet }
59    }
60
61    /// Derive an address using BIP44 standard path.
62    ///
63    /// Path format: `m/44'/60'/account'/change/address_index`
64    ///
65    /// # Arguments
66    ///
67    /// * `account` - Account index (usually 0)
68    /// * `change` - Whether this is a change address (usually false for Ethereum)
69    /// * `address_index` - Address index within the account
70    ///
71    /// # Errors
72    ///
73    /// Returns an error if derivation fails.
74    #[inline]
75    pub fn derive(
76        &self,
77        account: u32,
78        change: bool,
79        address_index: u32,
80    ) -> Result<DerivedAddress, Error> {
81        let change_val = i32::from(change);
82        let path = format!("m/44'/60'/{account}'/{change_val}/{address_index}");
83        self.derive_at_path(&path)
84    }
85
86    /// Derive an address at a custom derivation path.
87    ///
88    /// # Errors
89    ///
90    /// Returns an error if derivation fails.
91    pub fn derive_at_path(&self, path: &str) -> Result<DerivedAddress, Error> {
92        let private_key = self.derive_key(path)?;
93
94        let public_key = private_key.verifying_key();
95        let public_key_bytes = public_key.to_encoded_point(false);
96        let address = public_key_to_address(public_key_bytes.as_bytes());
97
98        Ok(DerivedAddress {
99            path: path.to_string(),
100            private_key_hex: Zeroizing::new(hex::encode(private_key.to_bytes())),
101            public_key_hex: hex::encode(public_key_bytes.as_bytes()),
102            address: to_checksum_address(&address),
103        })
104    }
105
106    /// Derive multiple addresses in sequence.
107    ///
108    /// # Arguments
109    ///
110    /// * `account` - Account index (usually 0)
111    /// * `change` - Whether these are change addresses
112    /// * `start_index` - Starting address index
113    /// * `count` - Number of addresses to derive
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if any derivation fails.
118    pub fn derive_many(
119        &self,
120        account: u32,
121        change: bool,
122        start_index: u32,
123        count: u32,
124    ) -> Result<Vec<DerivedAddress>, Error> {
125        (start_index..start_index + count)
126            .map(|index| self.derive(account, change, index))
127            .collect()
128    }
129
130    /// Derive an address using a specific wallet derivation style.
131    ///
132    /// This method supports different hardware/software wallet path formats:
133    /// - **Standard** (MetaMask/Trezor): `m/44'/60'/0'/0/{index}`
134    /// - **Ledger Live**: `m/44'/60'/{index}'/0/0`
135    /// - **Ledger Legacy**: `m/44'/60'/0'/{index}`
136    ///
137    /// # Arguments
138    ///
139    /// * `style` - The derivation style to use
140    /// * `index` - The address/account index
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if derivation fails.
145    ///
146    /// # Example
147    ///
148    /// ```ignore
149    /// use kobe_eth::{Deriver, DerivationStyle};
150    ///
151    /// let deriver = Deriver::new(&wallet);
152    ///
153    /// // Standard (MetaMask/Trezor) path
154    /// let addr = deriver.derive_with_style(DerivationStyle::Standard, 0).unwrap();
155    ///
156    /// // Ledger Live path
157    /// let addr = deriver.derive_with_style(DerivationStyle::LedgerLive, 0).unwrap();
158    /// ```
159    #[inline]
160    pub fn derive_with_style(
161        &self,
162        style: DerivationStyle,
163        index: u32,
164    ) -> Result<DerivedAddress, Error> {
165        self.derive_at_path(&style.path(index))
166    }
167
168    /// Derive multiple addresses using a specific wallet derivation style.
169    ///
170    /// # Arguments
171    ///
172    /// * `style` - The derivation style to use
173    /// * `start_index` - Starting index
174    /// * `count` - Number of addresses to derive
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if any derivation fails.
179    pub fn derive_many_with_style(
180        &self,
181        style: DerivationStyle,
182        start_index: u32,
183        count: u32,
184    ) -> Result<Vec<DerivedAddress>, Error> {
185        (start_index..start_index + count)
186            .map(|index| self.derive_with_style(style, index))
187            .collect()
188    }
189
190    /// Derive a private key at the given path using bip32 crate.
191    fn derive_key(&self, path: &str) -> Result<SigningKey, Error> {
192        // Parse derivation path
193        let derivation_path: DerivationPath = path
194            .parse()
195            .map_err(|e| Error::Derivation(format!("invalid derivation path: {e}")))?;
196
197        // Derive from seed directly using path
198        let derived = XPrv::derive_from_path(self.wallet.seed(), &derivation_path)
199            .map_err(|e| Error::Derivation(format!("key derivation failed: {e}")))?;
200
201        // Get signing key (XPrv wraps k256::ecdsa::SigningKey)
202        Ok(derived.private_key().clone())
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
211
212    fn test_wallet() -> Wallet {
213        Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap()
214    }
215
216    #[test]
217    fn test_derive_address() {
218        let wallet = test_wallet();
219        let deriver = Deriver::new(&wallet);
220        let addr = deriver.derive(0, false, 0).unwrap();
221
222        assert!(addr.address.starts_with("0x"));
223        assert_eq!(addr.address.len(), 42);
224        assert_eq!(addr.path, "m/44'/60'/0'/0/0");
225    }
226
227    #[test]
228    fn test_derive_multiple() {
229        let wallet = test_wallet();
230        let deriver = Deriver::new(&wallet);
231        let addrs = deriver.derive_many(0, false, 0, 5).unwrap();
232
233        assert_eq!(addrs.len(), 5);
234
235        // All addresses should be unique
236        let mut seen = alloc::vec::Vec::new();
237        for addr in &addrs {
238            assert!(!seen.contains(&addr.address));
239            seen.push(addr.address.clone());
240        }
241        assert_eq!(seen.len(), 5);
242    }
243
244    #[test]
245    fn test_deterministic_derivation() {
246        let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
247        let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
248
249        let deriver1 = Deriver::new(&wallet1);
250        let deriver2 = Deriver::new(&wallet2);
251
252        let addr1 = deriver1.derive(0, false, 0).unwrap();
253        let addr2 = deriver2.derive(0, false, 0).unwrap();
254
255        assert_eq!(addr1.address, addr2.address);
256    }
257
258    #[test]
259    fn test_passphrase_changes_addresses() {
260        let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
261        let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("password")).unwrap();
262
263        let deriver1 = Deriver::new(&wallet1);
264        let deriver2 = Deriver::new(&wallet2);
265
266        let addr1 = deriver1.derive(0, false, 0).unwrap();
267        let addr2 = deriver2.derive(0, false, 0).unwrap();
268
269        // Same mnemonic with different passphrase should produce different addresses
270        assert_ne!(addr1.address, addr2.address);
271    }
272
273    #[test]
274    fn test_derive_with_style_standard() {
275        let wallet = test_wallet();
276        let deriver = Deriver::new(&wallet);
277
278        let addr = deriver
279            .derive_with_style(DerivationStyle::Standard, 0)
280            .unwrap();
281        assert_eq!(addr.path, "m/44'/60'/0'/0/0");
282
283        let addr = deriver
284            .derive_with_style(DerivationStyle::Standard, 5)
285            .unwrap();
286        assert_eq!(addr.path, "m/44'/60'/0'/0/5");
287    }
288
289    #[test]
290    fn test_derive_with_style_ledger_live() {
291        let wallet = test_wallet();
292        let deriver = Deriver::new(&wallet);
293
294        let addr = deriver
295            .derive_with_style(DerivationStyle::LedgerLive, 0)
296            .unwrap();
297        assert_eq!(addr.path, "m/44'/60'/0'/0/0");
298
299        let addr = deriver
300            .derive_with_style(DerivationStyle::LedgerLive, 1)
301            .unwrap();
302        assert_eq!(addr.path, "m/44'/60'/1'/0/0");
303    }
304
305    #[test]
306    fn test_derive_with_style_ledger_legacy() {
307        let wallet = test_wallet();
308        let deriver = Deriver::new(&wallet);
309
310        let addr = deriver
311            .derive_with_style(DerivationStyle::LedgerLegacy, 0)
312            .unwrap();
313        assert_eq!(addr.path, "m/44'/60'/0'/0");
314
315        let addr = deriver
316            .derive_with_style(DerivationStyle::LedgerLegacy, 3)
317            .unwrap();
318        assert_eq!(addr.path, "m/44'/60'/0'/3");
319    }
320
321    #[test]
322    fn test_different_styles_produce_different_addresses() {
323        let wallet = test_wallet();
324        let deriver = Deriver::new(&wallet);
325
326        let standard = deriver
327            .derive_with_style(DerivationStyle::Standard, 1)
328            .unwrap();
329        let ledger_live = deriver
330            .derive_with_style(DerivationStyle::LedgerLive, 1)
331            .unwrap();
332        let ledger_legacy = deriver
333            .derive_with_style(DerivationStyle::LedgerLegacy, 1)
334            .unwrap();
335
336        // Different paths should produce different addresses
337        assert_ne!(standard.address, ledger_live.address);
338        assert_ne!(standard.address, ledger_legacy.address);
339        assert_ne!(ledger_live.address, ledger_legacy.address);
340    }
341
342    #[test]
343    fn test_derive_many_with_style() {
344        let wallet = test_wallet();
345        let deriver = Deriver::new(&wallet);
346
347        let addrs = deriver
348            .derive_many_with_style(DerivationStyle::LedgerLive, 0, 3)
349            .unwrap();
350
351        assert_eq!(addrs.len(), 3);
352        assert_eq!(addrs[0].path, "m/44'/60'/0'/0/0");
353        assert_eq!(addrs[1].path, "m/44'/60'/1'/0/0");
354        assert_eq!(addrs[2].path, "m/44'/60'/2'/0/0");
355    }
356}