crypto_wallet_gen/
bip32.rs1use 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 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 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 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}