1use std::str::FromStr;
24
25use bpstd::{DerivationIndex, DerivationPath, HardenedIndex, Idx, IdxBase, NormalIndex};
26
27#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Error, Display)]
29#[display(doc_comments)]
30pub enum ParseBip43Error {
31 InvalidBlockchainName(String),
34
35 UnhardenedBlockchainIndex(u32),
37
38 InvalidIdentityIndex(String),
40
41 InvalidPurposeIndex(String),
43
44 UnimplementedBip(u16),
47
48 UnrecognizedBipScheme,
50
51 InvalidBip43Scheme,
53
54 InvalidBip48Scheme,
56
57 InvalidDerivationPath(String),
59}
60
61#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)]
63#[cfg_attr(feature = "clap", derive(ValueEnum))]
64#[non_exhaustive]
65pub enum Bip43 {
66 #[display("bip44", alt = "m/44h")]
70 Bip44,
71
72 #[display("bip84", alt = "m/84h")]
76 Bip84,
77
78 #[display("bip49", alt = "m/49h")]
82 Bip49,
83
84 #[display("bip86", alt = "m/86h")]
88 Bip86,
89
90 #[display("bip45", alt = "m/45h")]
94 Bip45,
95
96 #[display("bip48-nested", alt = "m/48h//1h")]
101 Bip48Nested,
102
103 #[display("bip48-native", alt = "m/48h//2h")]
108 Bip48Native,
109
110 #[display("bip87", alt = "m/87h")]
114 Bip87,
115
116 #[display("bip43/{purpose}", alt = "m/{purpose}")]
120 #[cfg_attr(feature = "clap", clap(skip))]
121 Bip43 {
122 purpose: HardenedIndex,
124 },
125}
126
127impl FromStr for Bip43 {
128 type Err = ParseBip43Error;
129
130 fn from_str(s: &str) -> Result<Self, Self::Err> {
131 let s = s.to_lowercase();
132 let bip = s.strip_prefix("bip").or_else(|| s.strip_prefix("m/"));
133 Ok(match bip {
134 Some("44") => Bip43::Bip44,
135 Some("84") => Bip43::Bip84,
136 Some("49") => Bip43::Bip49,
137 Some("86") => Bip43::Bip86,
138 Some("45") => Bip43::Bip45,
139 Some(bip48) if bip48.starts_with("48//") => match bip48
140 .strip_prefix("48//")
141 .and_then(|index| HardenedIndex::from_str(index).ok())
142 {
143 Some(script_type) if script_type == 1u8 => Bip43::Bip48Nested,
144 Some(script_type) if script_type == 2u8 => Bip43::Bip48Native,
145 _ => {
146 return Err(ParseBip43Error::InvalidBip48Scheme);
147 }
148 },
149 Some("48-nested") => Bip43::Bip48Nested,
150 Some("48-native") => Bip43::Bip48Native,
151 Some("87") => Bip43::Bip87,
152 Some(bip43) if bip43.starts_with("43/") => match bip43.strip_prefix("43/") {
153 Some(purpose) => {
154 let purpose = HardenedIndex::from_str(purpose)
155 .map_err(|_| ParseBip43Error::InvalidPurposeIndex(purpose.to_owned()))?;
156 Bip43::Bip43 { purpose }
157 }
158 None => return Err(ParseBip43Error::InvalidBip43Scheme),
159 },
160 Some(_) | None => return Err(ParseBip43Error::UnrecognizedBipScheme),
161 })
162 }
163}
164
165impl Bip43 {
166 pub const PKH: Bip43 = Bip43::Bip44;
168 pub const WPKH_SH: Bip43 = Bip43::Bip49;
171 pub const WPKH: Bip43 = Bip43::Bip84;
173 pub const TR_SINGLE: Bip43 = Bip43::Bip86;
175 pub const MULTI_SH_SORTED: Bip43 = Bip43::Bip45;
177 pub const MULTI_WSH_SH: Bip43 = Bip43::Bip48Nested;
180 pub const MULTI_WSH: Bip43 = Bip43::Bip48Native;
183 pub const DESCRIPTOR: Bip43 = Bip43::Bip87;
185}
186
187pub trait DerivationStandard: Eq + Clone {
189 fn deduce(derivation: &DerivationPath) -> Option<Self>
192 where Self: Sized;
193
194 fn purpose(&self) -> Option<HardenedIndex>;
196
197 fn account_depth(&self) -> Option<u8>;
203
204 fn coin_type_depth(&self) -> Option<u8>;
210
211 fn is_account_last_hardened(&self) -> Option<bool>;
218
219 fn is_testnet(&self, path: &DerivationPath) -> Result<bool, Option<DerivationIndex>>;
222
223 fn extract_coin_type(
233 &self,
234 path: &DerivationPath,
235 ) -> Result<HardenedIndex, Option<NormalIndex>> {
236 let coin = self.coin_type_depth().and_then(|i| path.get(i as usize)).ok_or(None)?;
237 match coin {
238 DerivationIndex::Normal(idx) => Err(Some(*idx)),
239 DerivationIndex::Hardened(idx) => Ok(*idx),
240 }
241 }
242
243 fn extract_account_index(
253 &self,
254 path: &DerivationPath,
255 ) -> Result<HardenedIndex, Option<NormalIndex>> {
256 let coin = self.account_depth().and_then(|i| path.get(i as usize)).ok_or(None)?;
257 match coin {
258 DerivationIndex::Normal(idx) => Err(Some(*idx)),
259 DerivationIndex::Hardened(idx) => Ok(*idx),
260 }
261 }
262
263 fn account_template_string(&self, testnet: bool) -> String;
266
267 fn to_origin_derivation(&self, testnet: bool) -> DerivationPath<HardenedIndex>;
269
270 fn to_account_derivation(
272 &self,
273 account_index: HardenedIndex,
274 testnet: bool,
275 ) -> DerivationPath<HardenedIndex>;
276
277 fn to_key_derivation(
280 &self,
281 account_index: HardenedIndex,
282 testnet: bool,
283 keychain: NormalIndex,
284 index: NormalIndex,
285 ) -> DerivationPath;
286}
287
288impl DerivationStandard for Bip43 {
289 fn deduce(derivation: &DerivationPath) -> Option<Bip43> {
290 let mut iter = derivation.into_iter();
291 let first = iter.next().map(HardenedIndex::try_from).transpose().ok()??;
292 let fourth = iter.nth(3).map(HardenedIndex::try_from);
293 Some(match (first.child_number(), fourth) {
294 (44, ..) => Bip43::Bip44,
295 (84, ..) => Bip43::Bip84,
296 (49, ..) => Bip43::Bip49,
297 (86, ..) => Bip43::Bip86,
298 (45, ..) => Bip43::Bip45,
299 (87, ..) => Bip43::Bip87,
300 (48, Some(Ok(script_type))) if script_type == 1u8 => Bip43::Bip48Nested,
301 (48, Some(Ok(script_type))) if script_type == 2u8 => Bip43::Bip48Native,
302 (48, _) => return None,
303 (purpose, ..) if derivation.len() > 2 && purpose > 2 => Bip43::Bip43 {
304 purpose: HardenedIndex::hardened(purpose as u16),
305 },
306 _ => return None,
307 })
308 }
309
310 fn purpose(&self) -> Option<HardenedIndex> {
311 Some(match self {
312 Bip43::Bip44 => HardenedIndex::hardened(44),
313 Bip43::Bip84 => HardenedIndex::hardened(84),
314 Bip43::Bip49 => HardenedIndex::hardened(49),
315 Bip43::Bip86 => HardenedIndex::hardened(86),
316 Bip43::Bip45 => HardenedIndex::hardened(45),
317 Bip43::Bip48Nested | Bip43::Bip48Native => HardenedIndex::hardened(48),
318 Bip43::Bip87 => HardenedIndex::hardened(87),
319 Bip43::Bip43 { purpose } => *purpose,
320 })
321 }
322
323 fn account_depth(&self) -> Option<u8> {
324 Some(match self {
325 Bip43::Bip45 => return None,
326 Bip43::Bip44
327 | Bip43::Bip84
328 | Bip43::Bip49
329 | Bip43::Bip86
330 | Bip43::Bip87
331 | Bip43::Bip48Nested
332 | Bip43::Bip48Native
333 | Bip43::Bip43 { .. } => 3,
334 })
335 }
336
337 fn coin_type_depth(&self) -> Option<u8> {
338 Some(match self {
339 Bip43::Bip45 => return None,
340 Bip43::Bip44
341 | Bip43::Bip84
342 | Bip43::Bip49
343 | Bip43::Bip86
344 | Bip43::Bip87
345 | Bip43::Bip48Nested
346 | Bip43::Bip48Native
347 | Bip43::Bip43 { .. } => 2,
348 })
349 }
350
351 fn is_account_last_hardened(&self) -> Option<bool> {
352 Some(match self {
353 Bip43::Bip45 => false,
354 Bip43::Bip44
355 | Bip43::Bip84
356 | Bip43::Bip49
357 | Bip43::Bip86
358 | Bip43::Bip87
359 | Bip43::Bip43 { .. } => true,
360 Bip43::Bip48Nested | Bip43::Bip48Native => false,
361 })
362 }
363
364 fn is_testnet(&self, path: &DerivationPath) -> Result<bool, Option<DerivationIndex>> {
365 match self.extract_coin_type(path) {
366 Err(None) => Err(None),
367 Err(Some(idx)) => Err(Some(idx.into())),
368 Ok(HardenedIndex::ZERO) => Ok(false),
369 Ok(HardenedIndex::ONE) => Ok(true),
370 Ok(idx) => Err(Some(idx.into())),
371 }
372 }
373
374 fn account_template_string(&self, testnet: bool) -> String {
375 let coin_type = if testnet { HardenedIndex::ONE } else { HardenedIndex::ZERO };
376 match self {
377 Bip43::Bip45
378 | Bip43::Bip44
379 | Bip43::Bip84
380 | Bip43::Bip49
381 | Bip43::Bip86
382 | Bip43::Bip87
383 | Bip43::Bip43 { .. } => format!("{:#}/{}/*h", self, coin_type),
384 Bip43::Bip48Nested => {
385 format!("{:#}", self).replace("//", &format!("/{}/*h/", coin_type))
386 }
387 Bip43::Bip48Native => {
388 format!("{:#}", self).replace("//", &format!("/{}/*h/", coin_type))
389 }
390 }
391 }
392
393 fn to_origin_derivation(&self, testnet: bool) -> DerivationPath<HardenedIndex> {
394 let mut path = Vec::with_capacity(2);
395 if let Some(purpose) = self.purpose() {
396 path.push(purpose)
397 }
398 path.push(if testnet { HardenedIndex::ONE } else { HardenedIndex::ZERO });
399 path.into()
400 }
401
402 fn to_account_derivation(
403 &self,
404 account_index: HardenedIndex,
405 testnet: bool,
406 ) -> DerivationPath<HardenedIndex> {
407 let mut path = Vec::with_capacity(4);
408 path.push(account_index);
409 if self == &Bip43::Bip48Native {
410 path.push(HardenedIndex::from(2u8));
411 } else if self == &Bip43::Bip48Nested {
412 path.push(HardenedIndex::ONE);
413 }
414 let mut derivation = self.to_origin_derivation(testnet);
415 derivation.extend(&path);
416 derivation
417 }
418
419 fn to_key_derivation(
420 &self,
421 account_index: HardenedIndex,
422 testnet: bool,
423 keychain: NormalIndex,
424 index: NormalIndex,
425 ) -> DerivationPath {
426 let mut derivation = self
427 .to_account_derivation(account_index, testnet)
428 .into_iter()
429 .map(DerivationIndex::from)
430 .collect::<DerivationPath>();
431 derivation.push(keychain.into());
432 derivation.push(index.into());
433 derivation
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn test_bip43_str_round_trip() {
443 fn assert_from_str_to_str(bip43: Bip43) {
444 let str = bip43.to_string();
445 let from_str = Bip43::from_str(&str).unwrap();
446
447 assert_eq!(bip43, from_str);
448 }
449
450 assert_from_str_to_str(Bip43::Bip44);
451 assert_from_str_to_str(Bip43::Bip84);
452 assert_from_str_to_str(Bip43::Bip49);
453 assert_from_str_to_str(Bip43::Bip86);
454 assert_from_str_to_str(Bip43::Bip45);
455 assert_from_str_to_str(Bip43::Bip48Nested);
456 assert_from_str_to_str(Bip43::Bip48Native);
457 assert_from_str_to_str(Bip43::Bip87);
458 assert_from_str_to_str(Bip43::Bip43 {
459 purpose: HardenedIndex::hardened(1),
460 });
461 }
462}