Skip to main content

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