crypto_wallet_gen/
bip32.rs

1use anyhow::Result;
2use bitcoin::network::constants::Network;
3use bitcoin::util::bip32::ExtendedPrivKey;
4use clap::arg_enum;
5use secp256k1::Secp256k1;
6use std::convert::TryFrom;
7use std::convert::TryInto;
8
9use crate::seed::Seed;
10
11arg_enum! {
12    #[derive(Debug, Clone, Copy)]
13    #[allow(clippy::upper_case_acronyms)]
14    pub enum CoinType {
15        // List: https://github.com/libbitcoin/libbitcoin-system/wiki/Altcoin-Version-Mappings#10-monero-xmr-bip-3944-technology-examples
16        BTC,
17        XMR,
18        ETH,
19    }
20}
21
22impl CoinType {
23    fn bip44_value(self) -> u32 {
24        match self {
25            Self::BTC => 0,
26            Self::ETH => 60,
27            Self::XMR => 128,
28        }
29    }
30}
31
32#[derive(Debug)]
33pub struct Bip44DerivationPath {
34    pub coin_type: CoinType,
35    pub account: u32,
36    pub change: Option<u32>,
37    pub address_index: Option<u32>,
38}
39
40impl TryFrom<Bip44DerivationPath> for bitcoin::util::bip32::DerivationPath {
41    type Error = anyhow::Error;
42
43    fn try_from(path: Bip44DerivationPath) -> Result<bitcoin::util::bip32::DerivationPath> {
44        use bitcoin::util::bip32::ChildNumber;
45        let mut path_vec = vec![
46            ChildNumber::from_hardened_idx(44).expect("44 is a valid index"),
47            ChildNumber::from_hardened_idx(path.coin_type.bip44_value())?,
48            ChildNumber::from_hardened_idx(path.account)?,
49        ];
50        if let Some(change) = path.change {
51            path_vec.push(ChildNumber::from_normal_idx(change)?);
52        } else {
53            assert!(
54                path.address_index.is_none(),
55                "address_index can only be set when change is set"
56            );
57        }
58        if let Some(address_index) = path.address_index {
59            path_vec.push(ChildNumber::from_normal_idx(address_index)?);
60        }
61        Ok(path_vec.into())
62    }
63}
64
65impl std::fmt::Display for Bip44DerivationPath {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        write!(
68            f,
69            "m/44'/{}'/{}'",
70            self.coin_type.bip44_value(),
71            self.account
72        )?;
73        if let Some(change) = self.change {
74            write!(f, "/{}", change)?;
75        } else {
76            assert!(
77                self.address_index.is_none(),
78                "address_index can only be set when change is set"
79            );
80        }
81        if let Some(address_index) = self.address_index {
82            write!(f, "/{}", address_index)?;
83        }
84        Ok(())
85    }
86}
87
88#[allow(clippy::upper_case_acronyms)]
89pub struct HDPrivKey {
90    ext_key: ExtendedPrivKey,
91}
92
93impl HDPrivKey {
94    pub fn new(master_seed: Seed) -> Result<Self> {
95        Ok(Self {
96            ext_key: ExtendedPrivKey::new_master(Network::Bitcoin, master_seed.to_bytes())?,
97        })
98    }
99
100    pub fn derive(&self, path: Bip44DerivationPath) -> Result<HDPrivKey> {
101        let secp256k1 = Secp256k1::new();
102        let path: bitcoin::util::bip32::DerivationPath = path.try_into()?;
103        Ok(HDPrivKey {
104            ext_key: self.ext_key.derive_priv(&secp256k1, &path)?,
105        })
106    }
107
108    pub fn key_part(&self) -> Seed {
109        Seed::from_bytes(self.ext_key.private_key.to_bytes())
110    }
111
112    pub fn to_base58(&self) -> String {
113        format!("{}", self.ext_key)
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn test_account0() {
123        // Generated with https://iancoleman.io/bip39/
124        let master_seed = hex::decode("04c3fca05109eb0d188971e66ba949a4a4547b6c0eceddcb3e796e6ddb7d489826901932dbab5d6aa71421de1d119b4d472a92702e2642b2d9259d4766d84284").unwrap();
125        let child_key = HDPrivKey::new(Seed::from_bytes(master_seed))
126            .unwrap()
127            .derive(Bip44DerivationPath {
128                coin_type: CoinType::BTC,
129                account: 0,
130                change: Some(0),
131                address_index: None,
132            })
133            .unwrap();
134        assert_eq!(
135            "xprvA1gz733iMcZ7hmAwuWdzw6suwn3ScGtpjGH7qzdFTKqtMvyRyBZ92n3fpvLahFnqXpA13NwPktkkCumeaRQpRg7iNkcvUoBu4T1eK4fhNDv",
136            child_key.to_base58(),
137        );
138    }
139
140    #[test]
141    fn test_account1() {
142        // Generated with https://iancoleman.io/bip39/
143        let master_seed = hex::decode("04c3fca05109eb0d188971e66ba949a4a4547b6c0eceddcb3e796e6ddb7d489826901932dbab5d6aa71421de1d119b4d472a92702e2642b2d9259d4766d84284").unwrap();
144        let child_key = HDPrivKey::new(Seed::from_bytes(master_seed))
145            .unwrap()
146            .derive(Bip44DerivationPath {
147                coin_type: CoinType::BTC,
148                account: 1,
149                change: Some(0),
150                address_index: None,
151            })
152            .unwrap();
153        assert_eq!(
154            "xprvA2M4iy8qw2abD2MqssXJvtVU1p9AHHFPiqcSZzj28Gt1ZGwJ4oXLGQUK1R7JYQgtHA54t3yiKtSGgSVHwvxA1YJV7R7pbUefWa6u1E61rbS",
155            child_key.to_base58(),
156        );
157    }
158}