wagyu_ethereum/
derivation_path.rs

1use crate::network::EthereumNetwork;
2use wagyu_model::derivation_path::{ChildIndex, DerivationPath, DerivationPathError};
3
4use std::convert::TryFrom;
5use std::{fmt, marker::PhantomData, str::FromStr};
6
7/// Represents a Ethereum derivation path
8#[derive(Clone, PartialEq, Eq)]
9pub enum EthereumDerivationPath<N: EthereumNetwork> {
10    /// Ethereum Standard - m/44'/60'/0'/0/{index}
11    Ethereum(ChildIndex),
12    /// Exodus - m/44'/60'/0'/0/{index}
13    Exodus(ChildIndex),
14    /// Jaxx - m/44'/60'/0'/0/{index}
15    Jaxx(ChildIndex),
16    /// Metamask - m/44'/60'/0'/0/{index}
17    MetaMask(ChildIndex),
18    /// MyEtherWallet - m/44'/60'/0'/0/{index}
19    MyEtherWallet(ChildIndex),
20    /// Trezor - m/44'/60'/0'/0/{index}
21    Trezor(ChildIndex),
22
23    /// KeepKey - m/44'/60'/{index}'/0/0
24    KeepKey(ChildIndex),
25    /// Ledger Live - m/44'/60'/{index}'/0/0
26    LedgerLive(ChildIndex),
27
28    /// Electrum - m/44'/60'/0'/{index}
29    Electrum(ChildIndex),
30    /// imToken - m/44'/60'/0'/{index}
31    ImToken(ChildIndex),
32    /// imToken - m/44'/60'/0'/{index}
33    LedgerLegacy(ChildIndex),
34
35    /// Custom Ethereum derivation path
36    Custom(Vec<ChildIndex>, PhantomData<N>),
37}
38
39impl<N: EthereumNetwork> DerivationPath for EthereumDerivationPath<N> {
40    /// Returns a child index vector given the derivation path.
41    fn to_vec(&self) -> Result<Vec<ChildIndex>, DerivationPathError> {
42        match self {
43            EthereumDerivationPath::Ethereum(index)
44            | EthereumDerivationPath::Exodus(index)
45            | EthereumDerivationPath::Jaxx(index)
46            | EthereumDerivationPath::MetaMask(index)
47            | EthereumDerivationPath::MyEtherWallet(index)
48            | EthereumDerivationPath::Trezor(index) => match index.is_normal() {
49                true => Ok(vec![
50                    N::HD_PURPOSE,
51                    N::HD_COIN_TYPE,
52                    ChildIndex::Hardened(0),
53                    ChildIndex::Normal(0),
54                    *index,
55                ]),
56                false => Err(DerivationPathError::ExpectedBIP44Path),
57            },
58
59            EthereumDerivationPath::KeepKey(index) | EthereumDerivationPath::LedgerLive(index) => {
60                match index.is_hardened() {
61                    true => Ok(vec![
62                        N::HD_PURPOSE,
63                        N::HD_COIN_TYPE,
64                        *index,
65                        ChildIndex::Normal(0),
66                        ChildIndex::Normal(0),
67                    ]),
68                    false => Err(DerivationPathError::ExpectedBIP44Path),
69                }
70            }
71
72            EthereumDerivationPath::Electrum(index)
73            | EthereumDerivationPath::ImToken(index)
74            | EthereumDerivationPath::LedgerLegacy(index) => match index.is_normal() {
75                true => Ok(vec![N::HD_PURPOSE, N::HD_COIN_TYPE, ChildIndex::Hardened(0), *index]),
76                false => Err(DerivationPathError::ExpectedValidEthereumDerivationPath),
77            },
78
79            EthereumDerivationPath::Custom(path, _) => match path.len() < 256 {
80                true => Ok(path.clone()),
81                false => Err(DerivationPathError::ExpectedValidEthereumDerivationPath),
82            },
83        }
84    }
85
86    /// Returns a derivation path given the child index vector.
87    fn from_vec(path: &Vec<ChildIndex>) -> Result<Self, DerivationPathError> {
88        if path.len() == 4 {
89            // Path length 4 - Electrum (default), imToken, LedgerLegacy
90            if path[0] == N::HD_PURPOSE
91                && path[1] == N::HD_COIN_TYPE
92                && path[2] == ChildIndex::Hardened(0)
93                && path[3].is_normal()
94            {
95                return Ok(EthereumDerivationPath::Electrum(path[3]));
96            }
97        }
98
99        if path.len() == 5 {
100            // Path length 5 - Ethereum (default), Exodus, Jaxx, MetaMask, MyEtherWallet, Trezor
101            if path[0] == N::HD_PURPOSE
102                && path[1] == N::HD_COIN_TYPE
103                && path[2] == ChildIndex::Hardened(0)
104                && path[3] == ChildIndex::Normal(0)
105                && path[4].is_normal()
106            {
107                return Ok(EthereumDerivationPath::Ethereum(path[4]));
108            }
109            // Path length 5 - KeepKey, LedgerLive (default)
110            if path[0] == ChildIndex::Hardened(49)
111                && path[1] == N::HD_COIN_TYPE
112                && path[2].is_hardened()
113                && path[3] == ChildIndex::Normal(0)
114                && path[4] == ChildIndex::Normal(0)
115            {
116                return Ok(EthereumDerivationPath::LedgerLive(path[2]));
117            }
118        }
119
120        // Path length i - Custom Ethereum derivation path
121        Ok(EthereumDerivationPath::Custom(path.to_vec(), PhantomData))
122    }
123}
124
125impl<N: EthereumNetwork> FromStr for EthereumDerivationPath<N> {
126    type Err = DerivationPathError;
127
128    fn from_str(path: &str) -> Result<Self, Self::Err> {
129        let mut parts = path.split("/");
130
131        if parts.next().unwrap() != "m" {
132            return Err(DerivationPathError::InvalidDerivationPath(path.to_string()));
133        }
134
135        let path: Result<Vec<ChildIndex>, Self::Err> = parts.map(str::parse).collect();
136        Self::from_vec(&path?)
137    }
138}
139
140impl<N: EthereumNetwork> TryFrom<Vec<ChildIndex>> for EthereumDerivationPath<N> {
141    type Error = DerivationPathError;
142
143    fn try_from(path: Vec<ChildIndex>) -> Result<Self, Self::Error> {
144        Self::from_vec(&path)
145    }
146}
147
148impl<'a, N: EthereumNetwork> TryFrom<&'a [ChildIndex]> for EthereumDerivationPath<N> {
149    type Error = DerivationPathError;
150
151    fn try_from(path: &'a [ChildIndex]) -> Result<Self, Self::Error> {
152        Self::try_from(path.to_vec())
153    }
154}
155
156impl<N: EthereumNetwork> fmt::Debug for EthereumDerivationPath<N> {
157    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158        fmt::Display::fmt(&self, f)
159    }
160}
161
162impl<N: EthereumNetwork> fmt::Display for EthereumDerivationPath<N> {
163    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
164        match self.to_vec() {
165            Ok(path) => {
166                f.write_str("m")?;
167                for index in path.iter() {
168                    f.write_str("/")?;
169                    fmt::Display::fmt(index, f)?;
170                }
171                Ok(())
172            }
173            Err(_) => Err(fmt::Error),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::network::*;
182    use wagyu_model::derivation_path::{ChildIndex, DerivationPathError};
183
184    use std::convert::TryInto;
185    use std::str::FromStr;
186
187    #[test]
188    fn valid_path() {
189        type N = Mainnet;
190
191        assert_eq!(
192            EthereumDerivationPath::<N>::from_str("m"),
193            Ok(vec![].try_into().unwrap())
194        );
195        assert_eq!(
196            EthereumDerivationPath::<N>::from_str("m/0"),
197            Ok(vec![ChildIndex::normal(0).unwrap()].try_into().unwrap())
198        );
199        assert_eq!(
200            EthereumDerivationPath::<N>::from_str("m/0/1"),
201            Ok(vec![ChildIndex::normal(0).unwrap(), ChildIndex::normal(1).unwrap()]
202                .try_into()
203                .unwrap())
204        );
205        assert_eq!(
206            EthereumDerivationPath::<N>::from_str("m/0/1/2"),
207            Ok(vec![
208                ChildIndex::normal(0).unwrap(),
209                ChildIndex::normal(1).unwrap(),
210                ChildIndex::normal(2).unwrap()
211            ]
212            .try_into()
213            .unwrap())
214        );
215        assert_eq!(
216            EthereumDerivationPath::<N>::from_str("m/0/1/2/3"),
217            Ok(vec![
218                ChildIndex::normal(0).unwrap(),
219                ChildIndex::normal(1).unwrap(),
220                ChildIndex::normal(2).unwrap(),
221                ChildIndex::normal(3).unwrap()
222            ]
223            .try_into()
224            .unwrap())
225        );
226
227        assert_eq!(
228            EthereumDerivationPath::<N>::from_str("m"),
229            Ok(vec![].try_into().unwrap())
230        );
231        assert_eq!(
232            EthereumDerivationPath::<N>::from_str("m/0'"),
233            Ok(vec![ChildIndex::hardened(0).unwrap()].try_into().unwrap())
234        );
235        assert_eq!(
236            EthereumDerivationPath::<N>::from_str("m/0'/1"),
237            Ok(vec![ChildIndex::hardened(0).unwrap(), ChildIndex::normal(1).unwrap()]
238                .try_into()
239                .unwrap())
240        );
241        assert_eq!(
242            EthereumDerivationPath::<N>::from_str("m/0'/1/2'"),
243            Ok(vec![
244                ChildIndex::hardened(0).unwrap(),
245                ChildIndex::normal(1).unwrap(),
246                ChildIndex::hardened(2).unwrap(),
247            ]
248            .try_into()
249            .unwrap())
250        );
251        assert_eq!(
252            EthereumDerivationPath::<N>::from_str("m/0'/1/2'/3"),
253            Ok(vec![
254                ChildIndex::hardened(0).unwrap(),
255                ChildIndex::normal(1).unwrap(),
256                ChildIndex::hardened(2).unwrap(),
257                ChildIndex::normal(3).unwrap(),
258            ]
259            .try_into()
260            .unwrap())
261        );
262        assert_eq!(
263            EthereumDerivationPath::<N>::from_str("m/0'/1/2'/3/4'"),
264            Ok(vec![
265                ChildIndex::hardened(0).unwrap(),
266                ChildIndex::normal(1).unwrap(),
267                ChildIndex::hardened(2).unwrap(),
268                ChildIndex::normal(3).unwrap(),
269                ChildIndex::hardened(4).unwrap(),
270            ]
271            .try_into()
272            .unwrap())
273        );
274
275        assert_eq!(
276            EthereumDerivationPath::<N>::from_str("m"),
277            Ok(vec![].try_into().unwrap())
278        );
279        assert_eq!(
280            EthereumDerivationPath::<N>::from_str("m/0h"),
281            Ok(vec![ChildIndex::hardened(0).unwrap()].try_into().unwrap())
282        );
283        assert_eq!(
284            EthereumDerivationPath::<N>::from_str("m/0h/1'"),
285            Ok(vec![ChildIndex::hardened(0).unwrap(), ChildIndex::hardened(1).unwrap()]
286                .try_into()
287                .unwrap())
288        );
289        assert_eq!(
290            EthereumDerivationPath::<N>::from_str("m/0'/1h/2'"),
291            Ok(vec![
292                ChildIndex::hardened(0).unwrap(),
293                ChildIndex::hardened(1).unwrap(),
294                ChildIndex::hardened(2).unwrap(),
295            ]
296            .try_into()
297            .unwrap())
298        );
299        assert_eq!(
300            EthereumDerivationPath::<N>::from_str("m/0h/1'/2h/3'"),
301            Ok(vec![
302                ChildIndex::hardened(0).unwrap(),
303                ChildIndex::hardened(1).unwrap(),
304                ChildIndex::hardened(2).unwrap(),
305                ChildIndex::hardened(3).unwrap(),
306            ]
307            .try_into()
308            .unwrap())
309        );
310        assert_eq!(
311            EthereumDerivationPath::<N>::from_str("m/0'/1h/2'/3h/4'"),
312            Ok(vec![
313                ChildIndex::hardened(0).unwrap(),
314                ChildIndex::hardened(1).unwrap(),
315                ChildIndex::hardened(2).unwrap(),
316                ChildIndex::hardened(3).unwrap(),
317                ChildIndex::hardened(4).unwrap(),
318            ]
319            .try_into()
320            .unwrap())
321        );
322    }
323
324    #[test]
325    fn invalid_path() {
326        type N = Mainnet;
327
328        assert_eq!(
329            EthereumDerivationPath::<N>::from_str("n"),
330            Err(DerivationPathError::InvalidDerivationPath("n".try_into().unwrap()))
331        );
332        assert_eq!(
333            EthereumDerivationPath::<N>::from_str("n/0"),
334            Err(DerivationPathError::InvalidDerivationPath("n/0".try_into().unwrap()))
335        );
336        assert_eq!(
337            EthereumDerivationPath::<N>::from_str("n/0/0"),
338            Err(DerivationPathError::InvalidDerivationPath("n/0/0".try_into().unwrap()))
339        );
340
341        assert_eq!(
342            EthereumDerivationPath::<N>::from_str("1"),
343            Err(DerivationPathError::InvalidDerivationPath("1".try_into().unwrap()))
344        );
345        assert_eq!(
346            EthereumDerivationPath::<N>::from_str("1/0"),
347            Err(DerivationPathError::InvalidDerivationPath("1/0".try_into().unwrap()))
348        );
349        assert_eq!(
350            EthereumDerivationPath::<N>::from_str("1/0/0"),
351            Err(DerivationPathError::InvalidDerivationPath("1/0/0".try_into().unwrap()))
352        );
353
354        assert_eq!(
355            EthereumDerivationPath::<N>::from_str("m/0x"),
356            Err(DerivationPathError::InvalidChildNumberFormat)
357        );
358        assert_eq!(
359            EthereumDerivationPath::<N>::from_str("m/0x0"),
360            Err(DerivationPathError::InvalidChildNumberFormat)
361        );
362        assert_eq!(
363            EthereumDerivationPath::<N>::from_str("m/0x00"),
364            Err(DerivationPathError::InvalidChildNumberFormat)
365        );
366
367        assert_eq!(
368            EthereumDerivationPath::<N>::from_str("0/m"),
369            Err(DerivationPathError::InvalidDerivationPath("0/m".try_into().unwrap()))
370        );
371        assert_eq!(
372            EthereumDerivationPath::<N>::from_str("m//0"),
373            Err(DerivationPathError::InvalidChildNumberFormat)
374        );
375        assert_eq!(
376            EthereumDerivationPath::<N>::from_str("m/2147483648"),
377            Err(DerivationPathError::InvalidChildNumber(2147483648))
378        );
379    }
380}