seed_utils/
lib.rs

1//! # seed-utils
2//!
3//! **Note:** The word `seed` is interchangeably used for bip39 mnemonics.
4//!
5//! - Derive bip85 child seeds
6//! - Derive bip32 root xpubs and xprvs from seeds
7//! - Derive account xpubs and xprvs
8//! - XOR seeds
9//! - Truncate (reduce entropy to keep first n words of a seed)
10//! - Extend (extend entropy to add words to a seed)
11//!
12use 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/// All errors in this crate.
28#[derive(Debug, PartialEq, Eq)]
29pub enum Error {
30    /// Word count is not 12, 18 or 24.
31    BadWordCount,
32    /// Wrong checksum or unknown words.
33    BadSeed,
34    /// Bip32 errors like bad child numbers, derivation paths, base58 encoding and length.
35    Bip32,
36    /// Bip85 error for invalid index or byte length.
37    Bip85,
38    /// Word count is higher than expected.
39    WordCountTooHigh,
40    /// Word count is lower than expected.
41    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/// Valid number of words in a mnemonic.
98#[derive(Debug, PartialEq, Eq)]
99pub enum WordCount {
100    /// 12 Words
101    Words12,
102    /// 18 Words
103    Words18,
104    /// 24 Words
105    Words24,
106}
107
108impl WordCount {
109    /// Returns the number that `self` represents.
110    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
132/// Derives child seeds of `seed` with an index range of `[start, end)`. Each seed's word count will be exactly `word_count`.
133/// Returns list of tuples containing the derived seeds and their indexes.
134pub 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
158/// Extends a `seed`'s number of words to the desired length `word_count` by enxtending its entropy.
159/// The returned new seed will start with the same words as `seed`.
160pub fn extend_seed<S>(seed: S, word_count: &WordCount) -> Result<Mnemonic, Error>
161where
162    S: AsRef<str>,
163{
164    // Check if seed can be extended
165    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    // Determine length of new entropy
171    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    // Generate entropy
180    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
188/// Truncates a `seed`'s number of words to `word_count` by truncating its entropy.
189pub fn truncate_seed<S>(seed: S, word_count: &WordCount) -> Result<Mnemonic, Error>
190where
191    S: AsRef<str>,
192{
193    // Return early if seed is shorter than desired length
194    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    // Truncate entropy
200    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
210/// XORs multiple seeds and returns the resulting seed or `None` if `seeds` is empty.
211/// Can fail if a seed is not a valid [bip39::Mnemonic].
212pub 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
222/// Derives account extended public keys of a `seed` with an index range `[start, end)` and the derivation path of `version`.
223/// Returns a tuple of the derivation path and its derived xpub.
224pub 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
242/// Derives account extended private keys of a `seed` with an index range `[start, end)` and the derivation path of `version`.
243/// Returns a tuple of the derivation path and its derived xprv.
244pub 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
271/// Derives the master public key of a `seed` at the bip32 root.
272pub 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
282/// Derives the master private key of a `seed` at the bip32 root.
283pub 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
294/// Parses a `seed` string to a [bip39::Mnemonic].
295fn parse_seed<S>(seed: S) -> Result<Mnemonic, Error>
296where
297    S: AsRef<str>,
298{
299    Ok(Mnemonic::from_str(seed.as_ref())?)
300}
301
302/// Returns the bip32 derivation path of a xpub/xprv version.
303fn 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        // With 12 Words
359        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        // With 18 Words
373        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        // With 24 Words
384        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        // With start non 0
395        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        // From 12 to 12
422        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        // From 12 to 18
429        let word_count = WordCount::Words18;
430        let result = extend_seed(seed, &word_count).unwrap();
431        assert_eq!(result.word_count(), 18);
432
433        // From 12 to 24
434        let word_count = WordCount::Words24;
435        let result = extend_seed(seed, &word_count).unwrap();
436        assert_eq!(result.word_count(), 24);
437
438        // From 18 to 12
439        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        // From 18 to 18
445        let word_count = WordCount::Words18;
446        let result = extend_seed(seed, &word_count).unwrap();
447        assert_eq!(result.to_string(), seed);
448
449        // From 18 to 24
450        let word_count = WordCount::Words24;
451        let result = extend_seed(seed, &word_count).unwrap();
452        assert_eq!(result.word_count(), 24);
453
454        // From 24 to 12
455        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        // From 24 to 18
461        let word_count = WordCount::Words18;
462        let result = extend_seed(seed, &word_count);
463        assert!(result.is_err());
464
465        // From 24 to 24
466        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        // From 12 to 12
474        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        // From 12 to 18 -> err
481        let word_count = WordCount::Words18;
482        let result = truncate_seed(seed, &word_count);
483        assert!(result.is_err());
484
485        // From 12 to 24 -> err
486        let word_count = WordCount::Words24;
487        let result = truncate_seed(seed, &word_count);
488        assert!(result.is_err());
489
490        // From 18 to 12
491        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        // From 18 to 18
497        let word_count = WordCount::Words18;
498        let result = truncate_seed(seed, &word_count).unwrap();
499        assert_eq!(result.to_string(), seed);
500
501        // From 18 to 24 -> err
502        let word_count = WordCount::Words24;
503        let result = truncate_seed(seed, &word_count);
504        assert!(result.is_err());
505
506        // From 24 to 12
507        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        // From 24 to 18
513        let word_count = WordCount::Words18;
514        let result = truncate_seed(seed, &word_count).unwrap();
515        assert_eq!(result.word_count(), 18);
516
517        // From 24 to 24
518        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        // No seeds -> None
536        let result = xor_seeds(&seeds).unwrap();
537        assert!(result.is_none());
538
539        // One seed -> same seed
540        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        // More seeds -> correct XOR
546        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        // xprv
581        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        // yprv
592        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        // zprv
603        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        // xpub
622        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        // ypub
633        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        // zpub
644        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        // xpub
681        let version = Version::Xpub;
682        let path = derivation_path_from_version(&version).unwrap();
683        assert_eq!(path, path44);
684
685        // xprv
686        let version = Version::Xprv;
687        let path = derivation_path_from_version(&version).unwrap();
688        assert_eq!(path, path44);
689
690        // ypub
691        let version = Version::Ypub;
692        let path = derivation_path_from_version(&version).unwrap();
693        assert_eq!(path, path49);
694
695        // yprv
696        let version = Version::Yprv;
697        let path = derivation_path_from_version(&version).unwrap();
698        assert_eq!(path, path49);
699
700        // zpub
701        let version = Version::Zpub;
702        let path = derivation_path_from_version(&version).unwrap();
703        assert_eq!(path, path84);
704
705        // zprv
706        let version = Version::Zprv;
707        let path = derivation_path_from_version(&version).unwrap();
708        assert_eq!(path, path84);
709
710        // tpub
711        let version = Version::Tpub;
712        let path = derivation_path_from_version(&version).unwrap();
713        assert_eq!(path, path44_test);
714
715        // tprv
716        let version = Version::Tprv;
717        let path = derivation_path_from_version(&version).unwrap();
718        assert_eq!(path, path44_test);
719
720        // upub
721        let version = Version::Upub;
722        let path = derivation_path_from_version(&version).unwrap();
723        assert_eq!(path, path49_test);
724
725        // uprv
726        let version = Version::Uprv;
727        let path = derivation_path_from_version(&version).unwrap();
728        assert_eq!(path, path49_test);
729
730        // vpub
731        let version = Version::Vpub;
732        let path = derivation_path_from_version(&version).unwrap();
733        assert_eq!(path, path84_test);
734
735        // vprv
736        let version = Version::Vprv;
737        let path = derivation_path_from_version(&version).unwrap();
738        assert_eq!(path, path84_test);
739
740        // Multisig -> err
741        let version = Version::ZpubMultisig;
742        let path = derivation_path_from_version(&version);
743        assert!(path.is_err());
744    }
745}