1use std::str::FromStr;
13
14use bip85::bip39::{self, Mnemonic};
15use bitcoin::secp256k1::Secp256k1;
16use bitcoin::util::bip32::{self, ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey};
17use bitcoin::Network;
18use rand::{thread_rng, Rng};
19use seed_xor::SeedXor;
20use std::fmt;
21use xyzpub::Version;
22
23const ENTROPY_BYTES_24_WORDS: usize = 32;
24const ENTROPY_BYTES_18_WORDS: usize = 24;
25const ENTROPY_BYTES_12_WORDS: usize = 16;
26
27#[derive(Debug, PartialEq, Eq)]
29pub enum Error {
30 BadWordCount,
32 BadSeed,
34 Bip32,
36 Bip85,
38 WordCountTooHigh,
40 WordCountTooLow,
42}
43
44impl fmt::Display for Error {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::BadWordCount => write!(f, "Word count needs to be either 12, 18 or 24"),
48 Self::BadSeed => write!(
49 f,
50 "Seed is invalid because of a bad checksum or unknown words"
51 ),
52 Self::Bip32 => write!(
53 f,
54 "Bip32 error like bad child numbers, derivation paths, base58 encoding or length"
55 ),
56 Self::Bip85 => write!(f, "Bip85 error for invalid indexes or byte lengths."),
57 Self::WordCountTooHigh => {
58 write!(
59 f,
60 "Word count of seed is higher than expected for the operation"
61 )
62 }
63 Self::WordCountTooLow => {
64 write!(
65 f,
66 "Word count of seed is lower than expected for the operation"
67 )
68 }
69 }
70 }
71}
72
73impl From<bip39::Error> for Error {
74 fn from(e: bip39::Error) -> Self {
75 match e {
76 bip39::Error::BadWordCount(_) => Self::BadWordCount,
77 _ => Self::BadSeed,
78 }
79 }
80}
81
82impl From<bip32::Error> for Error {
83 fn from(_: bip32::Error) -> Self {
84 Self::Bip32
85 }
86}
87
88impl From<bip85::Error> for Error {
89 fn from(e: bip85::Error) -> Self {
90 match e {
91 bip85::Error::InvalidWordCount(_) => Self::BadWordCount,
92 _ => Self::Bip85,
93 }
94 }
95}
96
97#[derive(Debug, PartialEq, Eq)]
99pub enum WordCount {
100 Words12,
102 Words18,
104 Words24,
106}
107
108impl WordCount {
109 pub fn count(&self) -> u8 {
111 match self {
112 WordCount::Words12 => 12,
113 WordCount::Words18 => 18,
114 WordCount::Words24 => 24,
115 }
116 }
117}
118
119impl FromStr for WordCount {
120 type Err = Error;
121
122 fn from_str(s: &str) -> Result<Self, Self::Err> {
123 match s {
124 "12" => Ok(WordCount::Words12),
125 "18" => Ok(WordCount::Words18),
126 "24" => Ok(WordCount::Words24),
127 _ => Err(Error::BadWordCount),
128 }
129 }
130}
131
132pub fn derive_child_seeds<S>(
135 seed: S,
136 (start, mut end): (u32, u32),
137 word_count: &WordCount,
138) -> Result<Vec<(u32, Mnemonic)>, Error>
139where
140 S: AsRef<str>,
141{
142 if end < start {
143 end = start;
144 }
145 let xprv = derive_root_xprv(seed)?;
146 let secp = bip85::bitcoin::secp256k1::Secp256k1::new();
147
148 let mut result: Vec<(u32, Mnemonic)> = Vec::with_capacity(end as usize - start as usize);
149
150 for i in start..end {
151 let mnemonic = bip85::to_mnemonic(&secp, &xprv, word_count.count() as u32, i)?;
152 result.push((i, mnemonic));
153 }
154
155 Ok(result)
156}
157
158pub fn extend_seed<S>(seed: S, word_count: &WordCount) -> Result<Mnemonic, Error>
161where
162 S: AsRef<str>,
163{
164 let parsed_seed = parse_seed(seed)?;
166 if parsed_seed.word_count() > word_count.count() as usize {
167 return Err(Error::WordCountTooHigh);
168 }
169
170 let mut entropy = parsed_seed.to_entropy();
172 let mut rand = thread_rng();
173 let new_entropy_count = match word_count {
174 WordCount::Words12 => 0,
175 WordCount::Words18 => ENTROPY_BYTES_18_WORDS - entropy.len(),
176 WordCount::Words24 => ENTROPY_BYTES_24_WORDS - entropy.len(),
177 };
178
179 let more_entropy = std::iter::repeat(())
181 .map(|()| rand.gen::<u8>())
182 .take(new_entropy_count);
183 entropy.extend(more_entropy);
184
185 Ok(Mnemonic::from_entropy(&entropy)?)
186}
187
188pub fn truncate_seed<S>(seed: S, word_count: &WordCount) -> Result<Mnemonic, Error>
190where
191 S: AsRef<str>,
192{
193 let parsed_seed = parse_seed(seed)?;
195 if parsed_seed.word_count() < word_count.count() as usize {
196 return Err(Error::WordCountTooLow);
197 }
198
199 let mut entropy = parsed_seed.to_entropy();
201 match word_count {
202 WordCount::Words12 => entropy.truncate(ENTROPY_BYTES_12_WORDS),
203 WordCount::Words18 => entropy.truncate(ENTROPY_BYTES_18_WORDS),
204 WordCount::Words24 => (),
205 }
206
207 Ok(Mnemonic::from_entropy(&entropy)?)
208}
209
210pub fn xor_seeds(seeds: &[&str]) -> Result<Option<Mnemonic>, Error> {
213 let mut mnemonics: Vec<Mnemonic> = Vec::with_capacity(seeds.len());
214 for seed in seeds {
215 let mnemonic = Mnemonic::from_str(seed)?;
216 mnemonics.push(mnemonic);
217 }
218
219 Ok(mnemonics.into_iter().reduce(|a, b| a.xor(&b)))
220}
221
222pub fn derive_xpubs_from_seed<S>(
225 seed: S,
226 (start, end): (u32, u32),
227 version: &Version,
228) -> Result<Vec<(DerivationPath, ExtendedPubKey)>, Error>
229where
230 S: AsRef<str>,
231{
232 let xprvs = derive_xprvs_from_seed(seed, (start, end), version)?;
233 let secp = Secp256k1::new();
234 let xpubs = xprvs
235 .into_iter()
236 .map(move |(i, xprv)| (i, ExtendedPubKey::from_private(&secp, &xprv)))
237 .collect();
238
239 Ok(xpubs)
240}
241
242pub fn derive_xprvs_from_seed<S>(
245 seed: S,
246 (start, mut end): (u32, u32),
247 version: &Version,
248) -> Result<Vec<(DerivationPath, ExtendedPrivKey)>, Error>
249where
250 S: AsRef<str>,
251{
252 if end < start {
253 end = start;
254 }
255 let secp = Secp256k1::new();
256 let master = derive_root_xprv(seed)?;
257 let path = derivation_path_from_version(version)?;
258 let mut result: Vec<(DerivationPath, ExtendedPrivKey)> =
259 Vec::with_capacity(end as usize - start as usize);
260
261 for i in start..end {
262 let child = ChildNumber::from_hardened_idx(i)?;
263 let child_path = path.child(child);
264 let derived = master.derive_priv(&secp, &child_path)?;
265 result.push((child_path, derived));
266 }
267
268 Ok(result)
269}
270
271pub fn derive_root_xpub<S>(seed: S) -> Result<ExtendedPubKey, Error>
273where
274 S: AsRef<str>,
275{
276 let xprv = derive_root_xprv(seed)?;
277 let secp = Secp256k1::new();
278
279 Ok(ExtendedPubKey::from_private(&secp, &xprv))
280}
281
282pub fn derive_root_xprv<S>(seed: S) -> Result<ExtendedPrivKey, Error>
284where
285 S: AsRef<str>,
286{
287 let parsed_seed = parse_seed(seed)?;
288 let entropy = parsed_seed.to_seed("");
289 let xprv = ExtendedPrivKey::new_master(Network::Bitcoin, &entropy)?;
290
291 Ok(xprv)
292}
293
294fn parse_seed<S>(seed: S) -> Result<Mnemonic, Error>
296where
297 S: AsRef<str>,
298{
299 Ok(Mnemonic::from_str(seed.as_ref())?)
300}
301
302fn derivation_path_from_version(version: &Version) -> Result<DerivationPath, Error> {
304 match version {
305 Version::Xpub | Version::Xprv => Ok(DerivationPath::from_str("m/44h/0h")?),
306 Version::Ypub | Version::Yprv => Ok(DerivationPath::from_str("m/49h/0h")?),
307 Version::Zpub | Version::Zprv => Ok(DerivationPath::from_str("m/84h/0h")?),
308 Version::Tpub | Version::Tprv => Ok(DerivationPath::from_str("m/44h/1h")?),
309 Version::Upub | Version::Uprv => Ok(DerivationPath::from_str("m/49h/1h")?),
310 Version::Vpub | Version::Vprv => Ok(DerivationPath::from_str("m/84h/1h")?),
311 _ => Err(Error::Bip32),
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use std::str::FromStr;
318
319 use bip85::bitcoin::util::bip32::DerivationPath;
320 use xyzpub::Version;
321
322 use crate::{
323 derivation_path_from_version, derive_child_seeds, derive_root_xprv, derive_root_xpub,
324 derive_xprvs_from_seed, derive_xpubs_from_seed, extend_seed, parse_seed, truncate_seed,
325 xor_seeds, WordCount,
326 };
327
328 #[test]
329 fn wordcount_count_returns_correct_number() {
330 let word_count_12 = WordCount::Words12;
331 let word_count_18 = WordCount::Words18;
332 let word_count_24 = WordCount::Words24;
333
334 assert_eq!(word_count_12.count(), 12);
335 assert_eq!(word_count_18.count(), 18);
336 assert_eq!(word_count_24.count(), 24);
337 }
338
339 #[test]
340 fn wordcount_from_str_returns_correct_wordcount() {
341 let word_count_12 = WordCount::from_str("12").unwrap();
342 let word_count_18 = WordCount::from_str("18").unwrap();
343 let word_count_24 = WordCount::from_str("24").unwrap();
344 let word_count_err = WordCount::from_str("10");
345
346 assert_eq!(word_count_12, WordCount::Words12);
347 assert_eq!(word_count_18, WordCount::Words18);
348 assert_eq!(word_count_24, WordCount::Words24);
349 assert!(word_count_err.is_err());
350 }
351
352 #[test]
353 fn derive_child_seeds_returns_correct_seeds() {
354 let seed = "almost talk bulk high steel flush siege intact liberty radar journey bullet little olympic suffer neck clock glad furnace undo outdoor useful feature mobile";
355 let start = 0;
356 let end = 9;
357
358 let word_count = WordCount::Words12;
360 let result = derive_child_seeds(seed, (start, end), &word_count).unwrap();
361 let mut expected_index = start;
362 let child_seed_0 =
363 "loyal utility atom boat debris blush skull rare cool bamboo stage ritual";
364 assert_eq!(result.get(0).unwrap().1.to_string(), child_seed_0);
365 for (i, mnemonic) in result {
366 assert_eq!(i, expected_index);
367 assert_eq!(mnemonic.word_count(), word_count.count() as usize);
368 expected_index += 1;
369 }
370 assert_eq!(expected_index, end);
371
372 let word_count = WordCount::Words18;
374 let result = derive_child_seeds(seed, (start, end), &word_count).unwrap();
375 let mut expected_index = start;
376 for (i, mnemonic) in result {
377 assert_eq!(i, expected_index);
378 assert_eq!(mnemonic.word_count(), word_count.count() as usize);
379 expected_index += 1;
380 }
381 assert_eq!(expected_index, end);
382
383 let word_count = WordCount::Words24;
385 let result = derive_child_seeds(seed, (start, end), &word_count).unwrap();
386 let mut expected_index = start;
387 for (i, mnemonic) in result {
388 assert_eq!(i, expected_index);
389 assert_eq!(mnemonic.word_count(), word_count.count() as usize);
390 expected_index += 1;
391 }
392 assert_eq!(expected_index, end);
393
394 let start = 1;
396 let word_count = WordCount::Words24;
397 let result = derive_child_seeds(seed, (start, end), &word_count).unwrap();
398 let mut expected_index = start;
399 for (i, mnemonic) in result {
400 assert_eq!(i, expected_index);
401 assert_eq!(mnemonic.word_count(), word_count.count() as usize);
402 expected_index += 1;
403 }
404 assert_eq!(expected_index, end);
405 }
406
407 #[test]
408 fn derive_child_seeds_returns_err_when_seed_invalid() {
409 let seed = "almost talk bulk high steel flush siege intact liberty radar";
410 let start = 0;
411 let end = 9;
412 let word_count = WordCount::Words12;
413
414 let result = derive_child_seeds(seed, (start, end), &word_count);
415
416 assert!(result.is_err());
417 }
418
419 #[test]
420 fn extend_seed_extends_seed_to_word_count() {
421 let seed =
423 "tourist correct mango profit mom embody move thought deputy trophy excuse torch";
424 let word_count = WordCount::Words12;
425 let result = extend_seed(seed, &word_count).unwrap();
426 assert_eq!(result.to_string(), seed);
427
428 let word_count = WordCount::Words18;
430 let result = extend_seed(seed, &word_count).unwrap();
431 assert_eq!(result.word_count(), 18);
432
433 let word_count = WordCount::Words24;
435 let result = extend_seed(seed, &word_count).unwrap();
436 assert_eq!(result.word_count(), 24);
437
438 let seed = "decline wide tone omit home crime ridge student crop dog purchase actress inject eager hungry country actress shoot";
440 let word_count = WordCount::Words12;
441 let result = extend_seed(seed, &word_count);
442 assert!(result.is_err());
443
444 let word_count = WordCount::Words18;
446 let result = extend_seed(seed, &word_count).unwrap();
447 assert_eq!(result.to_string(), seed);
448
449 let word_count = WordCount::Words24;
451 let result = extend_seed(seed, &word_count).unwrap();
452 assert_eq!(result.word_count(), 24);
453
454 let seed = "seven snack chicken they course lawsuit century protect glimpse loan course thing nation ketchup fringe uniform kite else lawn that female impose silver citizen";
456 let word_count = WordCount::Words12;
457 let result = extend_seed(seed, &word_count);
458 assert!(result.is_err());
459
460 let word_count = WordCount::Words18;
462 let result = extend_seed(seed, &word_count);
463 assert!(result.is_err());
464
465 let word_count = WordCount::Words24;
467 let result = extend_seed(seed, &word_count).unwrap();
468 assert_eq!(result.to_string(), seed);
469 }
470
471 #[test]
472 fn truncate_seed_truncates_seed_to_word_count() {
473 let seed =
475 "tourist correct mango profit mom embody move thought deputy trophy excuse torch";
476 let word_count = WordCount::Words12;
477 let result = truncate_seed(seed, &word_count).unwrap();
478 assert_eq!(result.to_string(), seed);
479
480 let word_count = WordCount::Words18;
482 let result = truncate_seed(seed, &word_count);
483 assert!(result.is_err());
484
485 let word_count = WordCount::Words24;
487 let result = truncate_seed(seed, &word_count);
488 assert!(result.is_err());
489
490 let seed = "decline wide tone omit home crime ridge student crop dog purchase actress inject eager hungry country actress shoot";
492 let word_count = WordCount::Words12;
493 let result = truncate_seed(seed, &word_count).unwrap();
494 assert_eq!(result.word_count(), 12);
495
496 let word_count = WordCount::Words18;
498 let result = truncate_seed(seed, &word_count).unwrap();
499 assert_eq!(result.to_string(), seed);
500
501 let word_count = WordCount::Words24;
503 let result = truncate_seed(seed, &word_count);
504 assert!(result.is_err());
505
506 let seed = "seven snack chicken they course lawsuit century protect glimpse loan course thing nation ketchup fringe uniform kite else lawn that female impose silver citizen";
508 let word_count = WordCount::Words12;
509 let result = truncate_seed(seed, &word_count).unwrap();
510 assert_eq!(result.word_count(), 12);
511
512 let word_count = WordCount::Words18;
514 let result = truncate_seed(seed, &word_count).unwrap();
515 assert_eq!(result.word_count(), 18);
516
517 let word_count = WordCount::Words24;
519 let result = truncate_seed(seed, &word_count).unwrap();
520 assert_eq!(result.to_string(), seed);
521 }
522
523 #[test]
524 fn xor_seeds_returns_err_when_seed_invalid() {
525 let seeds = vec!["wagyu beef"];
526 let result = xor_seeds(&seeds);
527
528 assert!(result.is_err());
529 }
530
531 #[test]
532 fn xor_seeds_xors() {
533 let mut seeds: Vec<&str> = Vec::new();
534
535 let result = xor_seeds(&seeds).unwrap();
537 assert!(result.is_none());
538
539 let seed1 = "romance wink lottery autumn shop bring dawn tongue range crater truth ability miss spice fitness easy legal release recall obey exchange recycle dragon room";
541 seeds.push(seed1);
542 let result = xor_seeds(&seeds).unwrap().unwrap();
543 assert_eq!(result.to_string(), seed1);
544
545 let seed2 = "lion misery divide hurry latin fluid camp advance illegal lab pyramid unaware eager fringe sick camera series noodle toy crowd jeans select depth lounge";
547 let seed3 = "vault nominee cradle silk own frown throw leg cactus recall talent worry gadget surface shy planet purpose coffee drip few seven term squeeze educate";
548 let expected = "silent toe meat possible chair blossom wait occur this worth option bag nurse find fish scene bench asthma bike wage world quit primary indoor";
549 seeds.push(seed2);
550 seeds.push(seed3);
551 let result = xor_seeds(&seeds).unwrap().unwrap();
552 assert_eq!(result.to_string(), expected);
553 }
554
555 #[test]
556 fn derive_root_xprv_derives_root_derives_root_xprv() {
557 let seed =
558 "artefact enact unable pigeon bottom traffic art antenna country clip inspire borrow";
559 let expected = "xprv9s21ZrQH143K3rd3KuNUKxQMNEJsXTxUSuN9RQSm92oJEduoR4wnBneKzSdBDTnv9NtN9VJ2abs66gmM1rNbTdFKHoQPPMeyciwZZqsUbVC";
560 let result = derive_root_xprv(seed).unwrap();
561 assert_eq!(result.to_string(), expected);
562 }
563
564 #[test]
565 fn derive_root_xpub_derives_root_xpub() {
566 let seed =
567 "artefact enact unable pigeon bottom traffic art antenna country clip inspire borrow";
568 let expected = "xpub661MyMwAqRbcGLhWRvuUh6M5vG9MvvgKp8HkDnrNhNLH7SEwxcG2jaxoqgd5sQf8iQNLMV7F5kSczN52jPqaYRnACAZSjfGuX5sj3AdRDPM";
569 let result = derive_root_xpub(seed).unwrap();
570 assert_eq!(result.to_string(), expected);
571 }
572
573 #[test]
574 fn derive_xprvs_from_seed_derives_xprvs() {
575 let seed =
576 "artefact enact unable pigeon bottom traffic art antenna country clip inspire borrow";
577 let start = 0;
578 let end = 9;
579
580 let version = Version::Xprv;
582 let expected0 = "xprv9yG8MuRhkRHFKzBVybi9MP13e4xrMYo9hWcp9sUEfAwXcCDNz29CET74FAGwfk6yFceEHpuk5XUrmQnJSJW4dHcmJnwhJj6ee9h2kQUaDz5";
583 let expected1 = "xprv9yG8MuRhkRHFPTa4tJbapc9G4QLgfZtqKJ4xsk4p3nsVYDVVERMa9xmiwRPKkpxb9WRJAWakwVja38WRH9FTHbaXcBxsqaT7sk8GzTsKneJ";
584 let result = derive_xprvs_from_seed(seed, (start, end), &version).unwrap();
585 assert_eq!(result.len(), 9);
586 assert_eq!(result.get(0).unwrap().0.to_string(), "m/44'/0'/0'");
587 assert_eq!(result.get(0).unwrap().1.to_string(), expected0);
588 assert_eq!(result.get(1).unwrap().0.to_string(), "m/44'/0'/1'");
589 assert_eq!(result.get(1).unwrap().1.to_string(), expected1);
590
591 let version = Version::Yprv;
593 let expected0 = "xprv9yvxNCHWSBEQ7AtVCjf2jGK3qHULFkM55EqwcEktzUYLWMy9SiJJ2CTCK24m6sxpim2a7yYY9usaB1nLD6SvkupHCRZz7AE2U8ywMH2jbxU";
594 let expected1 = "xprv9yvxNCHWSBEQAqZpE9kUdUu7wbPUSvaC5YP43SyqxRLAHE5HBwe92omAxDMhfZrmV9m2vS46n9xk6JxBwAHq6GfwRto7VnshAwa2bmF33am";
595 let result = derive_xprvs_from_seed(seed, (start, end), &version).unwrap();
596 assert_eq!(result.len(), 9);
597 assert_eq!(result.get(0).unwrap().0.to_string(), "m/49'/0'/0'");
598 assert_eq!(result.get(0).unwrap().1.to_string(), expected0);
599 assert_eq!(result.get(1).unwrap().0.to_string(), "m/49'/0'/1'");
600 assert_eq!(result.get(1).unwrap().1.to_string(), expected1);
601
602 let version = Version::Zprv;
604 let expected0 = "xprv9zFNLT61T56ccvGNiPh3f1XiWSaGJTwUJYTLvGBdNGfhg2EddRjVwRAUV2LgdiVS5g8ffzUiucZzaZFGcjVjTXsTQGRgndqp5CG6wsG6cvx";
605 let expected1 = "xprv9zFNLT61T56cdVw4WVXh5KZFupHAkDXCKTL8oy4WCfznHsafM3wYuCedYQN91v5WYr2LPr2HX3ZrdspypqnXnHjqvNY117FRnKJZfjM3qBF";
606 let result = derive_xprvs_from_seed(seed, (start, end), &version).unwrap();
607 assert_eq!(result.len(), 9);
608 assert_eq!(result.get(0).unwrap().0.to_string(), "m/84'/0'/0'");
609 assert_eq!(result.get(0).unwrap().1.to_string(), expected0);
610 assert_eq!(result.get(1).unwrap().0.to_string(), "m/84'/0'/1'");
611 assert_eq!(result.get(1).unwrap().1.to_string(), expected1);
612 }
613
614 #[test]
615 fn derive_xpubs_from_seed_derives_xpubs() {
616 let seed =
617 "artefact enact unable pigeon bottom traffic art antenna country clip inspire borrow";
618 let start = 0;
619 let end = 9;
620
621 let version = Version::Xpub;
623 let expected0 = "xpub6CFUmQxbanqYYUFy5dF9iWwnC6oLm1X14jYQxFsrDWUWUzYXXZTSnFRY6T9e7V9R1762jkvCHAF7PVQ3rJtC5dwCCA7PkCqoxfrDBhyot63";
624 let expected1 = "xpub6CFUmQxbanqYbweXzL8bBk5zcSBB52cggWzZg8URc8QUR1pdmxfphm6CngQSPYbHJopuBLZg7qnMceyfUWN7r5RXeYQKEvArPzkstv1LiBy";
625 let result = derive_xpubs_from_seed(seed, (start, end), &version).unwrap();
626 assert_eq!(result.len(), 9);
627 assert_eq!(result.get(0).unwrap().0.to_string(), "m/44'/0'/0'");
628 assert_eq!(result.get(0).unwrap().1.to_string(), expected0);
629 assert_eq!(result.get(1).unwrap().0.to_string(), "m/44'/0'/1'");
630 assert_eq!(result.get(1).unwrap().1.to_string(), expected1);
631
632 let version = Version::Ypub;
634 let expected0 = "xpub6CvJmhpQGYnhKexxJmC36QFnPKJpfD4vSTmYQdAWYp5KPAJHzFcYZzmgAJQeMDK57oRiw1cpxVmzadQJDJ9L1LW6cCiWtXvF8jJmqicHeJi";
635 let expected1 = "xpub6CvJmhpQGYnhPKeHLBHUzcqrVdDxrPJ3SmJeqqPTWks9A2QRjUxPac5eoV5TtfnhKAQQgKZE377ZmoJc9oe6PSTnP8ETdRTg4tmgARXSUNE";
636 let result = derive_xpubs_from_seed(seed, (start, end), &version).unwrap();
637 assert_eq!(result.len(), 9);
638 assert_eq!(result.get(0).unwrap().0.to_string(), "m/49'/0'/0'");
639 assert_eq!(result.get(0).unwrap().1.to_string(), expected0);
640 assert_eq!(result.get(1).unwrap().0.to_string(), "m/49'/0'/1'");
641 assert_eq!(result.get(1).unwrap().1.to_string(), expected1);
642
643 let version = Version::Zpub;
645 let expected0 = "xpub6DEijxcuHSeuqQLqpRE429UT4UQkhvfKfmNwiebEvcCgYpZnAy3kVDUxLKqDpPCnho5hjvsoxLB88c3pPXero4YMsNnCeh6jjqhxyA6gT6Q";
646 let expected1 = "xpub6DEijxcuHSeuqz1XcX4hSTVzTr7f9gF3ggFjcMU7m1XmAfuotbFoSzy7PhzSPZA9xyYuAysaSrfjuF6caLTa81bAmreaHavVQakAuPKdYQj";
647 let result = derive_xpubs_from_seed(seed, (start, end), &version).unwrap();
648 assert_eq!(result.len(), 9);
649 assert_eq!(result.get(0).unwrap().0.to_string(), "m/84'/0'/0'");
650 assert_eq!(result.get(0).unwrap().1.to_string(), expected0);
651 assert_eq!(result.get(1).unwrap().0.to_string(), "m/84'/0'/1'");
652 assert_eq!(result.get(1).unwrap().1.to_string(), expected1);
653 }
654
655 #[test]
656 fn parse_seed_returns_mnemonic() {
657 let seed =
658 "artefact enact unable pigeon bottom traffic art antenna country clip inspire borrow";
659 let result = parse_seed(seed).unwrap();
660 assert_eq!(result.to_string(), seed);
661 }
662
663 #[test]
664 fn parse_seed_returns_err_when_seed_invalid() {
665 let seed =
666 "artefact enact unable pigeon bottom traffic art antenna country clip inspire antenna";
667 let result = parse_seed(seed);
668 assert!(result.is_err());
669 }
670
671 #[test]
672 fn derivation_path_from_version_returns_path() {
673 let path44 = DerivationPath::from_str("m/44h/0h").unwrap();
674 let path49 = DerivationPath::from_str("m/49h/0h").unwrap();
675 let path84 = DerivationPath::from_str("m/84h/0h").unwrap();
676 let path44_test = DerivationPath::from_str("m/44h/1h").unwrap();
677 let path49_test = DerivationPath::from_str("m/49h/1h").unwrap();
678 let path84_test = DerivationPath::from_str("m/84h/1h").unwrap();
679
680 let version = Version::Xpub;
682 let path = derivation_path_from_version(&version).unwrap();
683 assert_eq!(path, path44);
684
685 let version = Version::Xprv;
687 let path = derivation_path_from_version(&version).unwrap();
688 assert_eq!(path, path44);
689
690 let version = Version::Ypub;
692 let path = derivation_path_from_version(&version).unwrap();
693 assert_eq!(path, path49);
694
695 let version = Version::Yprv;
697 let path = derivation_path_from_version(&version).unwrap();
698 assert_eq!(path, path49);
699
700 let version = Version::Zpub;
702 let path = derivation_path_from_version(&version).unwrap();
703 assert_eq!(path, path84);
704
705 let version = Version::Zprv;
707 let path = derivation_path_from_version(&version).unwrap();
708 assert_eq!(path, path84);
709
710 let version = Version::Tpub;
712 let path = derivation_path_from_version(&version).unwrap();
713 assert_eq!(path, path44_test);
714
715 let version = Version::Tprv;
717 let path = derivation_path_from_version(&version).unwrap();
718 assert_eq!(path, path44_test);
719
720 let version = Version::Upub;
722 let path = derivation_path_from_version(&version).unwrap();
723 assert_eq!(path, path49_test);
724
725 let version = Version::Uprv;
727 let path = derivation_path_from_version(&version).unwrap();
728 assert_eq!(path, path49_test);
729
730 let version = Version::Vpub;
732 let path = derivation_path_from_version(&version).unwrap();
733 assert_eq!(path, path84_test);
734
735 let version = Version::Vprv;
737 let path = derivation_path_from_version(&version).unwrap();
738 assert_eq!(path, path84_test);
739
740 let version = Version::ZpubMultisig;
742 let path = derivation_path_from_version(&version);
743 assert!(path.is_err());
744 }
745}