cardano_sdk/wallet/
standard.rs

1//! Standard Cardano HD Wallet
2//!
3//! Based on HD stuff
4//!
5//! H(entropy) --- 1852' --- 1815' --- root-key --- 0' --- account-key --- type (0,1,2) --- keys
6//!
7use crate::chain::Ed25519KeyHash;
8use crate::crypto::key::*;
9use std::collections::BTreeMap;
10use thiserror::*;
11
12use bip39_dict::{Entropy, Mnemonics, ENGLISH};
13
14/// "Standard" Wallet for cardano.
15///
16pub struct StandardWallet {
17    account_key: HDPublicKey,
18    spending: PublicKeys,
19    changing: PublicKeys,
20    staking: PublicKeys,
21}
22
23pub struct StandardWalletKeys {
24    top_key: HDKey,
25    root_key: HDKey,
26    account_key: HDKey,
27    spending: SecretKeys,
28    changing: SecretKeys,
29    staking: SecretKeys,
30}
31
32pub struct PublicKeys {
33    #[allow(unused)]
34    hdkey: HDPublicKey,
35    pub latest: u32,
36    #[allow(unused)]
37    generated: Vec<(u32, Ed25519PublicKey, Ed25519KeyHash)>,
38    hash_to_idx: BTreeMap<Ed25519KeyHash, u32>,
39}
40
41pub struct SecretKeys {
42    hdkey: HDKey,
43    generated: Vec<(u32, Ed25519ExtendedKey)>,
44}
45
46#[derive(Error, Debug)]
47pub enum WalletError {
48    #[error("mnemonics error {0}")]
49    MnemonicsError(#[from] bip39_dict::MnemonicError),
50    #[error("entropy error {0}")]
51    EntropyError(#[from] bip39_dict::EntropyError),
52    #[error("unknown words count {0}")]
53    UnknownWordCount(usize),
54}
55
56const INDEX_SPENDING: Bip32Index = Bip32Index::soft(0);
57const INDEX_CHANGING: Bip32Index = Bip32Index::soft(1);
58const INDEX_STAKING: Bip32Index = Bip32Index::soft(2);
59
60/// Get the top key of a standard HD wallet deriving the slowed hash entropy
61fn words_to_top_key(words: &str, password: &[u8]) -> Result<HDKey, WalletError> {
62    let nb_words = words.chars().filter(|c| *c == ' ').count() + 1;
63    match nb_words {
64        24 => {
65            let mnemonics = Mnemonics::<24>::from_string(&ENGLISH, words).unwrap();
66            let entropy = Entropy::<32>::from_mnemonics::<24, 8>(&mnemonics).unwrap();
67            Ok(HDKey::from_bip39_entropy(&entropy.as_ref(), password))
68        }
69        21 => {
70            let mnemonics = Mnemonics::<21>::from_string(&ENGLISH, words).unwrap();
71            let entropy = Entropy::<28>::from_mnemonics::<21, 7>(&mnemonics).unwrap();
72            Ok(HDKey::from_bip39_entropy(&entropy.as_ref(), password))
73        }
74        18 => {
75            let mnemonics = Mnemonics::<18>::from_string(&ENGLISH, words).unwrap();
76            let entropy = Entropy::<24>::from_mnemonics::<18, 6>(&mnemonics).unwrap();
77            Ok(HDKey::from_bip39_entropy(&entropy.as_ref(), password))
78        }
79        15 => {
80            let mnemonics = Mnemonics::<15>::from_string(&ENGLISH, words).unwrap();
81            let entropy = Entropy::<20>::from_mnemonics::<15, 5>(&mnemonics).unwrap();
82            Ok(HDKey::from_bip39_entropy(&entropy.as_ref(), password))
83        }
84        12 => {
85            let mnemonics = Mnemonics::<12>::from_string(&ENGLISH, words).unwrap();
86            let entropy = Entropy::<16>::from_mnemonics::<12, 4>(&mnemonics).unwrap();
87            Ok(HDKey::from_bip39_entropy(&entropy.as_ref(), password))
88        }
89        _ => Err(WalletError::UnknownWordCount(nb_words)),
90    }
91}
92
93/// Get the root key of a standard HD wallet deriving the slowed hash entropy with 2 hard derivations
94/// with index 1852 and then index 1815.
95#[allow(dead_code)]
96pub(crate) fn words_to_root_key(words: &str, password: &[u8]) -> Result<HDKey, WalletError> {
97    let top_key = words_to_top_key(words, password)?;
98    let root_key = top_key
99        .bip32_derive(Bip32Index::hard(1852))
100        .bip32_derive(Bip32Index::hard(1815));
101    Ok(root_key)
102}
103
104impl SecretKeys {
105    pub fn new(hdkey: HDKey, lookahead: u32) -> Self {
106        let generated = (0..lookahead)
107            .map(|i| (i, hdkey.bip32_derive(Bip32Index::soft(i)).strip_hd()))
108            .collect();
109
110        Self { hdkey, generated }
111    }
112
113    pub fn to_public(&self) -> PublicKeys {
114        let hdkey = self.hdkey.to_public();
115        let generated = self
116            .generated
117            .iter()
118            .map(|(i, k)| {
119                let k = k.to_public();
120                let h = k.hash();
121                (*i, k, h)
122            })
123            .collect::<Vec<_>>();
124
125        let hash_to_idx = generated.iter().map(|(i, _, h)| (h.clone(), *i)).collect();
126        let latest = generated.iter().map(|(i, _, _)| *i).max().unwrap();
127
128        PublicKeys {
129            hdkey,
130            latest,
131            generated,
132            hash_to_idx,
133        }
134    }
135}
136
137impl PublicKeys {
138    pub fn new(hdkey: HDPublicKey, lookahead: u32) -> Self {
139        let generated = (0..lookahead)
140            .map(|i| {
141                let key = hdkey.bip32_derive(Bip32Index::soft(i)).strip_hd();
142                let key_hash = key.hash();
143                (i, key, key_hash)
144            })
145            .collect::<Vec<_>>();
146        let hash_to_idx = generated.iter().map(|(i, _, h)| (h.clone(), *i)).collect();
147        let latest = generated.iter().map(|(i, _, _)| *i).max().unwrap();
148
149        Self {
150            hdkey,
151            latest,
152            generated,
153            hash_to_idx,
154        }
155    }
156}
157
158impl StandardWalletKeys {
159    /// Create a standard wallet private parts from a set of english BIP39 words (12, 15, 18, 21, 24)
160    /// and a password
161    pub fn from_words(words: &str, password: &[u8]) -> Result<Self, WalletError> {
162        let top_key = words_to_top_key(words, password)?;
163        let root_key = top_key
164            .bip32_derive(Bip32Index::hard(1852))
165            .bip32_derive(Bip32Index::hard(1815));
166        let account_key = root_key.bip32_derive(Bip32Index::hard(0));
167
168        let spending_gen_key = account_key.bip32_derive(INDEX_SPENDING);
169        let changing_gen_key = account_key.bip32_derive(INDEX_CHANGING);
170        let staking_gen_key = account_key.bip32_derive(INDEX_STAKING);
171
172        Ok(Self {
173            top_key,
174            root_key,
175            account_key,
176            spending: SecretKeys::new(spending_gen_key, 10),
177            changing: SecretKeys::new(changing_gen_key, 10),
178            staking: SecretKeys::new(staking_gen_key, 10),
179        })
180    }
181
182    pub fn to_public(&self) -> StandardWallet {
183        StandardWallet {
184            account_key: self.account_key.to_public(),
185            spending: self.spending.to_public(),
186            changing: self.changing.to_public(),
187            staking: self.staking.to_public(),
188        }
189    }
190
191    pub fn top_key(&self) -> &HDKey {
192        &self.top_key
193    }
194
195    pub fn root_key(&self) -> &HDKey {
196        &self.root_key
197    }
198
199    pub fn account_key(&self) -> &HDKey {
200        &self.account_key
201    }
202}
203
204impl StandardWallet {
205    pub fn from_public_account_key(
206        account_key: HDPublicKey,
207        index_spend: u32,
208        index_change: u32,
209        index_staking: u32,
210    ) -> Self {
211        let spending_gen_key = account_key.bip32_derive(INDEX_SPENDING);
212        let changing_gen_key = account_key.bip32_derive(INDEX_CHANGING);
213        let staking_gen_key = account_key.bip32_derive(INDEX_STAKING);
214
215        Self {
216            account_key,
217            spending: PublicKeys::new(spending_gen_key, index_spend),
218            changing: PublicKeys::new(changing_gen_key, index_change),
219            staking: PublicKeys::new(staking_gen_key, index_staking),
220        }
221    }
222
223    pub fn account_key(&self) -> &HDPublicKey {
224        &self.account_key
225    }
226
227    pub fn indexes(&self) -> (u32, u32, u32) {
228        (
229            self.spending.latest,
230            self.changing.latest,
231            self.staking.latest,
232        )
233    }
234
235    pub fn match_credential(&self, h: &Ed25519KeyHash) -> Option<(u32, bool)> {
236        match self.spending.hash_to_idx.get(&h) {
237            None => match self.changing.hash_to_idx.get(&h) {
238                None => None,
239                Some(idx) => Some((*idx, true)),
240            },
241            Some(idx) => Some((*idx, false)),
242        }
243    }
244
245    /*
246    pub fn outputs_find_match<'a>(
247        &self,
248        txhash: &TxHash,
249        tx_outputs: &'a TransactionOutputs,
250    ) -> Vec<Matched<'a, ()>> {
251        let mut found = Vec::new();
252
253        let mut match_credential =
254            |tx_idx, txo, h: &Ed25519KeyHash| match self.spending.hash_to_idx.get(&h) {
255                None => match self.changing.hash_to_idx.get(&h) {
256                    None => {}
257                    Some(idx) => found.push(Matched {
258                        tx_body_hash: txhash.clone(),
259                        tx_idx,
260                        tx_output: txo,
261                        key_index: *idx,
262                        change: true,
263                    }),
264                },
265                Some(idx) => found.push(Matched {
266                    tx_body_hash: txhash.clone(),
267                    tx_idx,
268                    tx_output: txo,
269                    key_index: *idx,
270                    change: false,
271                }),
272            };
273
274        for (tx_idx, txo) in tx_outputs.iter_indexed() {
275            let address = match txo {
276                TransactionOutput::V1(s) => &s.address,
277                TransactionOutput::V2(s) => &s.address,
278            };
279            match Address::from_bytes(address.as_ref()) {
280                Err(_) => (),
281                Ok(Address::Entreprise(_, payment)) => match &payment {
282                    Credential::Key(h) => match_credential(tx_idx, txo, h),
283                    Credential::Script(_) => {}
284                },
285                Ok(Address::Base(Base {
286                    network: _,
287                    payment,
288                    stake: _,
289                })) => match &payment {
290                    Credential::Key(h) => match_credential(tx_idx, txo, h),
291                    Credential::Script(_) => {
292                        todo!()
293                    }
294                },
295                Ok(addr) => {
296                    println!("unsupported address {:?}", addr)
297                }
298            }
299        }
300        found
301    }
302    */
303}
304
305#[cfg(test)]
306mod tests {
307    use super::super::address::*;
308    use crate::chaininfo::ChainInfo;
309    use crate::crypto::key::*;
310
311    fn root_key_15() -> HDKey {
312        // art forum devote street sure rather head chuckle guard poverty release quote oak craft enemy
313        let entropy = [
314            0x0c, 0xcb, 0x74, 0xf3, 0x6b, 0x7d, 0xa1, 0x64, 0x9a, 0x81, 0x44, 0x67, 0x55, 0x22,
315            0xd4, 0xd8, 0x09, 0x7c, 0x64, 0x12,
316        ];
317        HDKey::from_bip39_entropy(&entropy, &[])
318    }
319
320    #[test]
321    fn bip32_15_base() {
322        // value from https://github.com/Emurgo/cardano-serialization-lib/blob/master/rust/src/address.rs#L837
323        // for interop purpose
324        let top = root_key_15()
325            .bip32_derive(Bip32Index::hard(1852))
326            .bip32_derive(Bip32Index::hard(1815));
327
328        let spend_key = top
329            .bip32_derive(Bip32Index::hard(0))
330            .bip32_derive(Bip32Index::soft(0))
331            .bip32_derive(Bip32Index::soft(0));
332        let stake_key = top
333            .bip32_derive(Bip32Index::hard(0))
334            .bip32_derive(Bip32Index::soft(2))
335            .bip32_derive(Bip32Index::soft(0));
336
337        let spend_pk = spend_key.to_public();
338        let stake_pk = stake_key.to_public();
339
340        let spend_cred = Credential::Key(spend_pk.hash());
341        let stake_cred = Credential::Key(stake_pk.hash());
342
343        let addr_net_0 = Address::Base(Base {
344            network: ChainInfo::TESTNET.network_id,
345            payment: spend_cred.clone(),
346            stake: stake_cred.clone(),
347        });
348        assert_eq!(addr_net_0.serialize().to_bech32(ChainInfo::TESTNET.bech32_hrp_address), "addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w");
349
350        let addr_net_3 = Address::Base(Base {
351            network: ChainInfo::MAINNET.network_id,
352            payment: spend_cred,
353            stake: stake_cred,
354        });
355        assert_eq!(addr_net_3.serialize().to_bech32(ChainInfo::MAINNET.bech32_hrp_address), "addr1q9u5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qld6xc3");
356    }
357}