ethers_wallet_rs/hd_wallet/
bip44.rs1use std::str::FromStr;
2
3use once_cell::sync::OnceCell;
4use regex::Regex;
5
6use thiserror::Error;
7
8fn 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#[derive(Debug, PartialEq, Clone)]
29pub enum Bip44Node {
30 Normal(u64),
31 Hardened(u64),
32}
33
34impl 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
94pub 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}