1use crate::error::HdError;
4use std::fmt;
5use std::str::FromStr;
6
7pub const HARDENED_BIT: u32 = 0x80000000;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ChildNumber {
13 Normal(u32),
15 Hardened(u32),
17}
18
19impl ChildNumber {
20 pub fn normal(index: u32) -> Result<Self, HdError> {
22 if index >= HARDENED_BIT {
23 return Err(HdError::InvalidChildNumber(index));
24 }
25 Ok(ChildNumber::Normal(index))
26 }
27
28 pub fn hardened(index: u32) -> Result<Self, HdError> {
30 if index >= HARDENED_BIT {
31 return Err(HdError::InvalidChildNumber(index));
32 }
33 Ok(ChildNumber::Hardened(index))
34 }
35
36 pub fn is_hardened(&self) -> bool {
38 matches!(self, ChildNumber::Hardened(_))
39 }
40
41 pub fn index(&self) -> u32 {
43 match self {
44 ChildNumber::Normal(i) | ChildNumber::Hardened(i) => *i,
45 }
46 }
47
48 pub fn raw_index(&self) -> u32 {
50 match self {
51 ChildNumber::Normal(i) => *i,
52 ChildNumber::Hardened(i) => i | HARDENED_BIT,
53 }
54 }
55}
56
57impl fmt::Display for ChildNumber {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 ChildNumber::Normal(i) => write!(f, "{}", i),
61 ChildNumber::Hardened(i) => write!(f, "{}'", i),
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct DerivationPath {
83 components: Vec<ChildNumber>,
84}
85
86impl DerivationPath {
87 pub fn master() -> Self {
89 Self { components: vec![] }
90 }
91
92 pub fn parse(path: &str) -> Result<Self, HdError> {
96 let path = path.trim();
97
98 if path.is_empty() || path == "m" || path == "M" {
100 return Ok(Self::master());
101 }
102
103 let path = if path.starts_with("m/") || path.starts_with("M/") {
105 &path[2..]
106 } else {
107 return Err(HdError::InvalidPath(
108 "Path must start with 'm/'".to_string(),
109 ));
110 };
111
112 let mut components = Vec::new();
113
114 for part in path.split('/') {
115 let part = part.trim();
116 if part.is_empty() {
117 continue;
118 }
119
120 let (index_str, hardened) = if part.ends_with('\'') || part.ends_with('h') || part.ends_with('H') {
121 (&part[..part.len() - 1], true)
122 } else {
123 (part, false)
124 };
125
126 let index: u32 = index_str.parse().map_err(|_| {
127 HdError::InvalidPath(format!("Invalid index: {}", index_str))
128 })?;
129
130 if index >= HARDENED_BIT {
131 return Err(HdError::InvalidPath(format!(
132 "Index too large: {}",
133 index
134 )));
135 }
136
137 let child = if hardened {
138 ChildNumber::Hardened(index)
139 } else {
140 ChildNumber::Normal(index)
141 };
142
143 components.push(child);
144 }
145
146 Ok(Self { components })
147 }
148
149 pub fn bip44_bitcoin(account: u32, change: u32, index: u32) -> Self {
151 Self {
152 components: vec![
153 ChildNumber::Hardened(44),
154 ChildNumber::Hardened(0), ChildNumber::Hardened(account),
156 ChildNumber::Normal(change),
157 ChildNumber::Normal(index),
158 ],
159 }
160 }
161
162 pub fn bip44_ethereum(account: u32, index: u32) -> Self {
164 Self {
165 components: vec![
166 ChildNumber::Hardened(44),
167 ChildNumber::Hardened(60), ChildNumber::Hardened(account),
169 ChildNumber::Normal(0),
170 ChildNumber::Normal(index),
171 ],
172 }
173 }
174
175 pub fn components(&self) -> &[ChildNumber] {
177 &self.components
178 }
179
180 pub fn has_hardened(&self) -> bool {
182 self.components.iter().any(|c| c.is_hardened())
183 }
184
185 pub fn depth(&self) -> u8 {
187 self.components.len() as u8
188 }
189
190 pub fn child(&self, child: ChildNumber) -> Self {
192 let mut components = self.components.clone();
193 components.push(child);
194 Self { components }
195 }
196}
197
198impl fmt::Display for DerivationPath {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(f, "m")?;
201 for component in &self.components {
202 write!(f, "/{}", component)?;
203 }
204 Ok(())
205 }
206}
207
208impl FromStr for DerivationPath {
209 type Err = HdError;
210
211 fn from_str(s: &str) -> Result<Self, Self::Err> {
212 Self::parse(s)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_parse_master() {
222 let path = DerivationPath::parse("m").unwrap();
223 assert!(path.components().is_empty());
224 }
225
226 #[test]
227 fn test_parse_bip44() {
228 let path = DerivationPath::parse("m/44'/0'/0'/0/0").unwrap();
229 assert_eq!(path.components().len(), 5);
230 assert!(path.components()[0].is_hardened());
231 assert!(path.components()[1].is_hardened());
232 assert!(path.components()[2].is_hardened());
233 assert!(!path.components()[3].is_hardened());
234 assert!(!path.components()[4].is_hardened());
235 }
236
237 #[test]
238 fn test_parse_h_notation() {
239 let path = DerivationPath::parse("m/44h/0h/0h/0/0").unwrap();
240 assert_eq!(path.to_string(), "m/44'/0'/0'/0/0");
241 }
242
243 #[test]
244 fn test_bip44_bitcoin() {
245 let path = DerivationPath::bip44_bitcoin(0, 0, 0);
246 assert_eq!(path.to_string(), "m/44'/0'/0'/0/0");
247 }
248
249 #[test]
250 fn test_bip44_ethereum() {
251 let path = DerivationPath::bip44_ethereum(0, 0);
252 assert_eq!(path.to_string(), "m/44'/60'/0'/0/0");
253 }
254
255 #[test]
256 fn test_roundtrip() {
257 let original = "m/44'/0'/0'/0/0";
258 let path = DerivationPath::parse(original).unwrap();
259 assert_eq!(path.to_string(), original);
260 }
261
262 #[test]
263 fn test_has_hardened() {
264 let path1 = DerivationPath::parse("m/44'/0'/0'/0/0").unwrap();
265 assert!(path1.has_hardened());
266
267 let path2 = DerivationPath::parse("m/0/1/2").unwrap();
268 assert!(!path2.has_hardened());
269 }
270}