Skip to main content

kobe_eth/
derivation_style.rs

1//! Derivation path styles for different wallet software.
2//!
3//! Different wallet software (`MetaMask`, Ledger, Trezor) use different BIP-44
4//! derivation paths. This module provides predefined styles for compatibility.
5
6use alloc::{format, string::String};
7use core::fmt;
8use core::str::FromStr;
9
10/// Ethereum derivation path styles for different wallet software.
11///
12/// Different hardware and software wallets use different derivation paths
13/// even though they all follow BIP-44 principles. This enum provides
14/// the most common styles for maximum compatibility.
15///
16/// # Path Specifications (as of 2024)
17///
18/// - **MetaMask/Trezor**: Standard BIP-44 `m/44'/60'/0'/0/{index}`
19/// - **Ledger Live**: Account-based `m/44'/60'/{index}'/0/0`
20/// - **Ledger Legacy**: MEW/MyCrypto compatible `m/44'/60'/0'/{index}`
21///
22/// # Example
23///
24/// ```
25/// use kobe_eth::DerivationStyle;
26///
27/// let style = DerivationStyle::LedgerLive;
28/// assert_eq!(style.path(0), "m/44'/60'/0'/0/0");
29/// assert_eq!(style.path(1), "m/44'/60'/1'/0/0");
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
32#[non_exhaustive]
33pub enum DerivationStyle {
34    /// Standard BIP-44 path used by `MetaMask` and Trezor.
35    ///
36    /// Path format: `m/44'/60'/0'/0/{index}`
37    ///
38    /// This is the most widely adopted standard where:
39    /// - Purpose: 44' (BIP-44)
40    /// - Coin type: 60' (Ethereum)
41    /// - Account: 0' (fixed)
42    /// - Change: 0 (external)
43    /// - Address index: variable
44    #[default]
45    Standard,
46
47    /// Ledger Live derivation path.
48    ///
49    /// Path format: `m/44'/60'/{index}'/0/0`
50    ///
51    /// Ledger Live treats each index as a separate account:
52    /// - Purpose: 44' (BIP-44)
53    /// - Coin type: 60' (Ethereum)
54    /// - Account: variable (hardened)
55    /// - Change: 0 (external)
56    /// - Address index: 0 (fixed)
57    LedgerLive,
58
59    /// Ledger Legacy derivation path (MEW/MyCrypto compatible).
60    ///
61    /// Path format: `m/44'/60'/0'/{index}`
62    ///
63    /// Used by older Ledger Chrome app and compatible with MEW/MyCrypto:
64    /// - Purpose: 44' (BIP-44)
65    /// - Coin type: 60' (Ethereum)
66    /// - Account: 0' (fixed)
67    /// - Address index: variable (at 4th level, non-hardened)
68    LedgerLegacy,
69}
70
71impl DerivationStyle {
72    /// Generate the derivation path string for a given index.
73    ///
74    /// # Arguments
75    ///
76    /// * `index` - The address/account index to derive
77    ///
78    /// # Returns
79    ///
80    /// A BIP-32 derivation path string.
81    #[must_use]
82    pub fn path(self, index: u32) -> String {
83        match self {
84            Self::Standard => format!("m/44'/60'/0'/0/{index}"),
85            Self::LedgerLive => format!("m/44'/60'/{index}'/0/0"),
86            Self::LedgerLegacy => format!("m/44'/60'/0'/{index}"),
87        }
88    }
89
90    /// Get the human-readable name of this derivation style.
91    #[must_use]
92    pub const fn name(self) -> &'static str {
93        match self {
94            Self::Standard => "Standard (MetaMask/Trezor)",
95            Self::LedgerLive => "Ledger Live",
96            Self::LedgerLegacy => "Ledger Legacy (MEW/MyCrypto)",
97        }
98    }
99
100    /// Get a short identifier for CLI usage.
101    #[must_use]
102    pub const fn id(self) -> &'static str {
103        match self {
104            Self::Standard => "standard",
105            Self::LedgerLive => "ledger-live",
106            Self::LedgerLegacy => "ledger-legacy",
107        }
108    }
109
110    /// Get all available derivation styles.
111    #[must_use]
112    pub const fn all() -> &'static [Self] {
113        &[Self::Standard, Self::LedgerLive, Self::LedgerLegacy]
114    }
115}
116
117impl fmt::Display for DerivationStyle {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "{}", self.name())
120    }
121}
122
123impl FromStr for DerivationStyle {
124    type Err = ParseDerivationStyleError;
125
126    fn from_str(s: &str) -> Result<Self, Self::Err> {
127        match s.to_lowercase().as_str() {
128            "standard" | "metamask" | "trezor" | "bip44" => Ok(Self::Standard),
129            "ledger-live" | "ledgerlive" | "live" => Ok(Self::LedgerLive),
130            "ledger-legacy" | "ledgerlegacy" | "legacy" | "mew" | "mycrypto" => {
131                Ok(Self::LedgerLegacy)
132            }
133            _ => Err(ParseDerivationStyleError(s.into())),
134        }
135    }
136}
137
138/// Error returned when parsing an invalid derivation style string.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct ParseDerivationStyleError(pub(crate) String);
141
142impl fmt::Display for ParseDerivationStyleError {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(
145            f,
146            "invalid derivation style '{}', expected one of: standard, ledger-live, ledger-legacy",
147            self.0
148        )
149    }
150}
151
152#[cfg(feature = "std")]
153impl std::error::Error for ParseDerivationStyleError {}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_standard_paths() {
161        let style = DerivationStyle::Standard;
162        assert_eq!(style.path(0), "m/44'/60'/0'/0/0");
163        assert_eq!(style.path(1), "m/44'/60'/0'/0/1");
164        assert_eq!(style.path(10), "m/44'/60'/0'/0/10");
165    }
166
167    #[test]
168    fn test_ledger_live_paths() {
169        let style = DerivationStyle::LedgerLive;
170        assert_eq!(style.path(0), "m/44'/60'/0'/0/0");
171        assert_eq!(style.path(1), "m/44'/60'/1'/0/0");
172        assert_eq!(style.path(10), "m/44'/60'/10'/0/0");
173    }
174
175    #[test]
176    fn test_ledger_legacy_paths() {
177        let style = DerivationStyle::LedgerLegacy;
178        assert_eq!(style.path(0), "m/44'/60'/0'/0");
179        assert_eq!(style.path(1), "m/44'/60'/0'/1");
180        assert_eq!(style.path(10), "m/44'/60'/0'/10");
181    }
182
183    #[test]
184    fn test_from_str() {
185        assert_eq!(
186            "standard".parse::<DerivationStyle>().unwrap(),
187            DerivationStyle::Standard
188        );
189        assert_eq!(
190            "metamask".parse::<DerivationStyle>().unwrap(),
191            DerivationStyle::Standard
192        );
193        assert_eq!(
194            "trezor".parse::<DerivationStyle>().unwrap(),
195            DerivationStyle::Standard
196        );
197        assert_eq!(
198            "ledger-live".parse::<DerivationStyle>().unwrap(),
199            DerivationStyle::LedgerLive
200        );
201        assert_eq!(
202            "ledger-legacy".parse::<DerivationStyle>().unwrap(),
203            DerivationStyle::LedgerLegacy
204        );
205        assert_eq!(
206            "mew".parse::<DerivationStyle>().unwrap(),
207            DerivationStyle::LedgerLegacy
208        );
209    }
210
211    #[test]
212    fn test_from_str_invalid() {
213        assert!("invalid".parse::<DerivationStyle>().is_err());
214    }
215
216    #[test]
217    fn test_default() {
218        assert_eq!(DerivationStyle::default(), DerivationStyle::Standard);
219    }
220}