ethers_wallet_rs/hd_wallet/
bip44.rs

1use std::str::FromStr;
2
3use once_cell::sync::OnceCell;
4use regex::Regex;
5
6use thiserror::Error;
7
8/// Path parsing regex
9fn get_path_regex() -> &'static Regex {
10    static REGEX: OnceCell<Regex> = OnceCell::new();
11
12    REGEX.get_or_init(|| {
13        Regex::new("^m/(\\d+'?)/(\\d+'?)/(\\d+'?)/([0,1])/(\\d+'?)$").expect("Compile path regex")
14    })
15}
16
17#[derive(Debug, Error)]
18pub enum Bip44Error {
19    #[error("Invalid bip44 node string, {0}")]
20    InvalidBip44NodeString(String),
21    #[error("Invalid bip44 path, {0}")]
22    InvalidPath(String),
23}
24
25static HARDENED_BIT: u64 = 0x80000000;
26
27/// BIP44 path number instance
28#[derive(Debug, PartialEq, Clone)]
29pub enum Bip44Node {
30    Normal(u64),
31    Hardened(u64),
32}
33
34// impl Into<u64> for Bip44Node {
35//     fn into(self) -> u64 {
36//         match self {
37//             Self::Normal(v) => v,
38//             Self::Hardened(v) => v | HARDENED_BIT,
39//         }
40//     }
41// }
42
43impl From<Bip44Node> for u64 {
44    fn from(node: Bip44Node) -> Self {
45        match node {
46            Bip44Node::Normal(v) => v,
47            Bip44Node::Hardened(v) => v | HARDENED_BIT,
48        }
49    }
50}
51
52impl<'a> From<&'a Bip44Node> for u64 {
53    fn from(node: &'a Bip44Node) -> Self {
54        match node {
55            Bip44Node::Normal(v) => *v,
56            Bip44Node::Hardened(v) => v | HARDENED_BIT,
57        }
58    }
59}
60
61impl From<u64> for Bip44Node {
62    fn from(value: u64) -> Self {
63        if (value & HARDENED_BIT) == HARDENED_BIT {
64            Bip44Node::Hardened(value | HARDENED_BIT)
65        } else {
66            Bip44Node::Normal(value)
67        }
68    }
69}
70
71impl FromStr for Bip44Node {
72    type Err = Bip44Error;
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let mut hardened = false;
75
76        let s = if s.ends_with("'") {
77            hardened = true;
78            s.trim_end_matches("'")
79        } else {
80            s
81        };
82
83        let value =
84            u64::from_str(s).map_err(|_| Bip44Error::InvalidBip44NodeString(s.to_string()))?;
85
86        if hardened {
87            Ok(Self::Hardened(value))
88        } else {
89            Ok(Self::Normal(value))
90        }
91    }
92}
93
94// // Deterministic bip44 path
95pub struct Bip44Path {
96    pub purpose: Bip44Node,
97    pub coin: Bip44Node,
98    pub account: Bip44Node,
99    pub change: Bip44Node,
100    pub address: Bip44Node,
101}
102
103impl FromStr for Bip44Path {
104    type Err = Bip44Error;
105
106    fn from_str(value: &str) -> Result<Self, Self::Err> {
107        if let Some(captures) = get_path_regex().captures(value) {
108            Ok(Bip44Path {
109                purpose: captures.get(1).unwrap().as_str().parse()?,
110                coin: captures.get(2).unwrap().as_str().parse()?,
111                account: captures.get(3).unwrap().as_str().parse()?,
112                change: captures.get(4).unwrap().as_str().parse()?,
113                address: captures.get(5).unwrap().as_str().parse()?,
114            })
115        } else {
116            Err(Bip44Error::InvalidPath(value.to_string()))
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::{Bip44Path, HARDENED_BIT};
124
125    #[test]
126    fn test_parse_path() {
127        let path: Bip44Path = "m/44'/60'/0'/0/1".parse().expect("eip44 path");
128
129        assert_eq!(u64::from(&path.purpose), 44u64 | HARDENED_BIT);
130
131        assert_eq!(u64::from(&path.coin), 60u64 | HARDENED_BIT);
132
133        assert_eq!(u64::from(&path.account), 0u64 | HARDENED_BIT);
134
135        assert_eq!(u64::from(&path.change), 0u64);
136
137        assert_eq!(u64::from(&path.address), 1);
138    }
139}