nym_validator_client/signing/
direct_wallet.rs1use crate::signing::signer::{OfflineSigner, SigningError};
5use crate::signing::{
6 derive_extended_private_key, derive_keypair, AccountData, Secp256k1Derivation, Secp256k1Keypair,
7};
8use bip32::XPrv;
9use cosmrs::bip32::DerivationPath;
10use cosmrs::tx;
11use cosmrs::tx::SignDoc;
12use nym_config::defaults;
13use std::borrow::Cow;
14use thiserror::Error;
15use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
16
17#[derive(Debug, Error)]
18pub enum DirectSecp256k1HdWalletError {
19 #[error(transparent)]
20 SigningFailure(#[from] SigningError),
21
22 #[error("failed to derive child key: {source}")]
23 Bip32KeyDerivationFailure {
24 #[from]
25 source: bip32::Error,
26 },
27
28 #[error("There was an issue with bip39: {source}")]
29 Bip39Error {
30 #[from]
31 source: bip39::Error,
32 },
33
34 #[error("failed to derive accounts: {source}")]
35 AccountDerivationError { source: eyre::Report },
36}
37
38#[derive(Zeroize, ZeroizeOnDrop)]
40pub struct DirectSecp256k1HdWallet {
41 secret: bip39::Mnemonic,
43
44 #[zeroize(skip)]
46 accounts: Vec<AccountData>,
48}
49
50impl OfflineSigner for DirectSecp256k1HdWallet {
51 type Error = DirectSecp256k1HdWalletError;
52
53 fn get_accounts(&self) -> &[AccountData] {
54 &self.accounts
55 }
56
57 fn sign_direct_with_account(
58 &self,
59 signer: &AccountData,
60 sign_doc: SignDoc,
61 ) -> Result<tx::Raw, Self::Error> {
62 sign_doc
63 .sign(&signer.private_key)
64 .map_err(|source| SigningError::SigningFailure { source }.into())
65 }
66}
67
68impl DirectSecp256k1HdWallet {
69 pub fn builder(prefix: &str) -> DirectSecp256k1HdWalletBuilder {
70 DirectSecp256k1HdWalletBuilder::new(prefix)
71 }
72
73 #[deprecated(
75 note = "this function can potentially panic if accounts can't be derived correctly. please use .checked_from_mnemonic() instead"
76 )]
77 pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Self {
78 #[allow(deprecated)]
81 DirectSecp256k1HdWalletBuilder::new(prefix).build(mnemonic)
82 }
83
84 pub fn checked_from_mnemonic(
86 prefix: &str,
87 mnemonic: bip39::Mnemonic,
88 ) -> Result<Self, DirectSecp256k1HdWalletError> {
89 DirectSecp256k1HdWalletBuilder::new(prefix).try_build(mnemonic)
90 }
91
92 pub fn generate(prefix: &str, word_count: usize) -> Result<Self, DirectSecp256k1HdWalletError> {
93 let mneomonic = bip39::Mnemonic::generate(word_count)?;
94 Self::checked_from_mnemonic(prefix, mneomonic)
95 }
96
97 pub fn secret(&self) -> &bip39::Mnemonic {
98 &self.secret
99 }
100
101 #[deprecated(
102 note = "use either .secret() for obtaining &bip39::Mnemonic or .mnemonic_string() for Zeroizing wrapper around the String"
103 )]
104 pub fn mnemonic(&self) -> String {
105 self.secret.to_string()
106 }
107
108 pub fn mnemonic_string(&self) -> Zeroizing<String> {
109 Zeroizing::new(self.secret.to_string())
110 }
111
112 pub fn account_seed<'a, P: Into<Cow<'a, str>>>(
113 &self,
114 bip39_password: P,
115 ) -> Zeroizing<[u8; 64]> {
116 Zeroizing::new(self.secret.to_seed(bip39_password))
117 }
118
119 #[deprecated(
121 note = "use derive_extended_private_key_with_password to ensure correct derivation if used bip39 password"
122 )]
123 pub fn derive_extended_private_key(
124 &self,
125 hd_path: &DerivationPath,
126 ) -> Result<XPrv, DirectSecp256k1HdWalletError> {
127 let seed = self.account_seed("");
128 derive_extended_private_key(seed, hd_path)
129 }
130
131 pub fn derive_keypair<'a, P: Into<Cow<'a, str>>>(
132 &self,
133 hd_path: &DerivationPath,
134 bip39_password: P,
135 ) -> Result<Secp256k1Keypair, DirectSecp256k1HdWalletError> {
136 let seed = self.account_seed(bip39_password);
137 derive_keypair(seed, hd_path)
138 }
139
140 pub fn derive_extended_private_key_with_password<'a, P: Into<Cow<'a, str>>>(
141 &self,
142 hd_path: &DerivationPath,
143 bip39_password: P,
144 ) -> Result<XPrv, DirectSecp256k1HdWalletError> {
145 let seed = self.account_seed(bip39_password);
146 derive_extended_private_key(seed, hd_path)
147 }
148}
149
150#[must_use]
151#[derive(Zeroize)]
152#[zeroize(drop)]
153pub struct DirectSecp256k1HdWalletBuilder {
154 bip39_password: String,
156
157 #[zeroize(skip)]
159 hd_paths: Vec<DerivationPath>,
160
161 prefix: String,
163}
164
165impl DirectSecp256k1HdWalletBuilder {
166 pub fn new(prefix: &str) -> Self {
167 DirectSecp256k1HdWalletBuilder {
168 bip39_password: String::new(),
169 hd_paths: vec![defaults::COSMOS_DERIVATION_PATH.parse().unwrap()],
170 prefix: prefix.into(),
171 }
172 }
173
174 pub fn with_bip39_password<S: Into<String>>(mut self, password: S) -> Self {
175 self.bip39_password = password.into();
176 self
177 }
178
179 pub fn with_hd_path(mut self, path: DerivationPath) -> Self {
180 self.hd_paths.push(path);
181 self
182 }
183
184 pub fn with_hd_paths(mut self, hd_paths: Vec<DerivationPath>) -> Self {
185 self.hd_paths = hd_paths;
186 self
187 }
188
189 pub fn with_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
190 self.prefix = prefix.into();
191 self
192 }
193
194 #[deprecated(
195 note = "this function can potentially panic if accounts can't be derived correctly. please use .try_build() instead"
196 )]
197 pub fn build(self, mnemonic: bip39::Mnemonic) -> DirectSecp256k1HdWallet {
198 #[allow(clippy::expect_used)]
201 self.try_build(mnemonic)
202 .expect("account derivation failure")
203 }
204
205 pub fn try_build(
206 self,
207 mnemonic: bip39::Mnemonic,
208 ) -> Result<DirectSecp256k1HdWallet, DirectSecp256k1HdWalletError> {
209 let seed = Zeroizing::new(mnemonic.to_seed(&self.bip39_password));
210 let prefix = self.prefix.clone();
211 let accounts = self
212 .hd_paths
213 .iter()
214 .map(|hd_path| {
215 Secp256k1Derivation {
216 hd_path: hd_path.clone(),
217 prefix: prefix.clone(),
218 }
219 .try_derive_account(&seed)
220 })
221 .collect::<Result<_, _>>()?;
222
223 Ok(DirectSecp256k1HdWallet {
224 accounts,
225 secret: mnemonic,
226 })
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use nym_network_defaults::NymNetworkDetails;
233
234 use super::*;
235
236 #[test]
237 fn generating_account_addresses() -> anyhow::Result<()> {
238 let mnemonics = ["crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove",
240 "acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel",
241 "step income throw wheat mobile ship wave drink pool sudden upset jaguar bar globe rifle spice frost bless glimpse size regular carry aspect ball"];
242 let prefix = NymNetworkDetails::new_mainnet()
243 .chain_details
244 .bech32_account_prefix;
245
246 let addrs = [
247 "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
248 "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
249 "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf",
250 ];
251 for (idx, mnemonic) in mnemonics.iter().enumerate() {
252 let wallet =
253 DirectSecp256k1HdWallet::checked_from_mnemonic(&prefix, mnemonic.parse()?)?;
254 assert_eq!(wallet.signer_addresses()[0], addrs[idx].parse().unwrap());
255 }
256 Ok(())
257 }
258}