1use std::{fmt, ops, path::Path};
2
3use async_trait::async_trait;
4use elliptic_curve::rand_core;
5use fuel_crypto::{Message, PublicKey, SecretKey, Signature};
6use fuels_core::{
7 traits::Signer,
8 types::{
9 bech32::{Bech32Address, FUEL_BECH32_HRP},
10 coin_type_id::CoinTypeId,
11 errors::{error, Result},
12 input::Input,
13 transaction_builders::TransactionBuilder,
14 AssetId,
15 },
16};
17use rand::{CryptoRng, Rng};
18use zeroize::{Zeroize, ZeroizeOnDrop};
19
20use crate::{accounts_utils::try_provider_error, provider::Provider, Account, ViewOnlyAccount};
21
22pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'";
23
24#[derive(Clone)]
32pub struct Wallet {
33 pub(crate) address: Bech32Address,
36 provider: Option<Provider>,
37}
38
39#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
45pub struct WalletUnlocked {
46 #[zeroize(skip)]
47 wallet: Wallet,
48 pub(crate) private_key: SecretKey,
49}
50
51impl Wallet {
52 pub fn from_address(address: Bech32Address, provider: Option<Provider>) -> Self {
54 Self { address, provider }
55 }
56
57 pub fn provider(&self) -> Option<&Provider> {
58 self.provider.as_ref()
59 }
60
61 pub fn set_provider(&mut self, provider: Provider) {
62 self.provider = Some(provider);
63 }
64
65 pub fn address(&self) -> &Bech32Address {
66 &self.address
67 }
68
69 pub fn unlock(self, private_key: SecretKey) -> WalletUnlocked {
74 WalletUnlocked {
75 wallet: self,
76 private_key,
77 }
78 }
79}
80
81#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
82impl ViewOnlyAccount for Wallet {
83 fn address(&self) -> &Bech32Address {
84 self.address()
85 }
86
87 fn try_provider(&self) -> Result<&Provider> {
88 self.provider.as_ref().ok_or_else(try_provider_error)
89 }
90
91 async fn get_asset_inputs_for_amount(
92 &self,
93 asset_id: AssetId,
94 amount: u64,
95 excluded_coins: Option<Vec<CoinTypeId>>,
96 ) -> Result<Vec<Input>> {
97 Ok(self
98 .get_spendable_resources(asset_id, amount, excluded_coins)
99 .await?
100 .into_iter()
101 .map(Input::resource_signed)
102 .collect::<Vec<Input>>())
103 }
104}
105
106impl WalletUnlocked {
107 pub fn lock(mut self) -> Wallet {
109 self.private_key.zeroize();
110 self.wallet.clone()
111 }
112
113 pub fn set_provider(&mut self, provider: Provider) {
118 self.wallet.set_provider(provider);
119 }
120
121 pub fn new_random(provider: Option<Provider>) -> Self {
123 let mut rng = rand::thread_rng();
124 let private_key = SecretKey::random(&mut rng);
125 Self::new_from_private_key(private_key, provider)
126 }
127
128 pub fn new_from_private_key(private_key: SecretKey, provider: Option<Provider>) -> Self {
130 let public = PublicKey::from(&private_key);
131 let hashed = public.hash();
132 let address = Bech32Address::new(FUEL_BECH32_HRP, hashed);
133 Wallet::from_address(address, provider).unlock(private_key)
134 }
135
136 pub fn new_from_mnemonic_phrase(phrase: &str, provider: Option<Provider>) -> Result<Self> {
139 let path = format!("{DEFAULT_DERIVATION_PATH_PREFIX}/0'/0/0");
140 Self::new_from_mnemonic_phrase_with_path(phrase, provider, &path)
141 }
142
143 pub fn new_from_mnemonic_phrase_with_path(
146 phrase: &str,
147 provider: Option<Provider>,
148 path: &str,
149 ) -> Result<Self> {
150 let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, path)?;
151
152 Ok(Self::new_from_private_key(secret_key, provider))
153 }
154
155 pub fn new_from_keystore<P, R, S>(
157 dir: P,
158 rng: &mut R,
159 password: S,
160 provider: Option<Provider>,
161 ) -> Result<(Self, String)>
162 where
163 P: AsRef<Path>,
164 R: Rng + CryptoRng + rand_core::CryptoRng,
165 S: AsRef<[u8]>,
166 {
167 let (secret, uuid) =
168 eth_keystore::new(dir, rng, password, None).map_err(|e| error!(Other, "{e}"))?;
169
170 let secret_key = SecretKey::try_from(secret.as_slice()).expect("should have correct size");
171
172 let wallet = Self::new_from_private_key(secret_key, provider);
173
174 Ok((wallet, uuid))
175 }
176
177 pub fn encrypt<P, S>(&self, dir: P, password: S) -> Result<String>
180 where
181 P: AsRef<Path>,
182 S: AsRef<[u8]>,
183 {
184 let mut rng = rand::thread_rng();
185
186 eth_keystore::encrypt_key(dir, &mut rng, *self.private_key, password, None)
187 .map_err(|e| error!(Other, "{e}"))
188 }
189
190 pub fn load_keystore<P, S>(keypath: P, password: S, provider: Option<Provider>) -> Result<Self>
192 where
193 P: AsRef<Path>,
194 S: AsRef<[u8]>,
195 {
196 let secret =
197 eth_keystore::decrypt_key(keypath, password).map_err(|e| error!(Other, "{e}"))?;
198 let secret_key = SecretKey::try_from(secret.as_slice())
199 .expect("Decrypted key should have a correct size");
200 Ok(Self::new_from_private_key(secret_key, provider))
201 }
202
203 pub fn address(&self) -> &Bech32Address {
204 &self.address
205 }
206
207 #[cfg(feature = "test-helpers")]
209 pub fn secret_key(&self) -> &SecretKey {
210 &self.private_key
211 }
212}
213
214#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
215impl ViewOnlyAccount for WalletUnlocked {
216 fn address(&self) -> &Bech32Address {
217 self.wallet.address()
218 }
219
220 fn try_provider(&self) -> Result<&Provider> {
221 self.provider.as_ref().ok_or_else(try_provider_error)
222 }
223
224 async fn get_asset_inputs_for_amount(
225 &self,
226 asset_id: AssetId,
227 amount: u64,
228 excluded_coins: Option<Vec<CoinTypeId>>,
229 ) -> Result<Vec<Input>> {
230 self.wallet
231 .get_asset_inputs_for_amount(asset_id, amount, excluded_coins)
232 .await
233 }
234}
235
236impl Account for WalletUnlocked {
237 fn add_witnesses<Tb: TransactionBuilder>(&self, tb: &mut Tb) -> Result<()> {
238 tb.add_signer(self.clone())?;
239
240 Ok(())
241 }
242}
243
244#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
245#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
246impl Signer for WalletUnlocked {
247 async fn sign(&self, message: Message) -> Result<Signature> {
248 let sig = Signature::sign(&self.private_key, &message);
249
250 Ok(sig)
251 }
252
253 fn address(&self) -> &Bech32Address {
254 &self.address
255 }
256}
257
258impl fmt::Debug for Wallet {
259 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 f.debug_struct("Wallet")
261 .field("address", &self.address)
262 .finish()
263 }
264}
265
266impl ops::Deref for WalletUnlocked {
267 type Target = Wallet;
268 fn deref(&self) -> &Self::Target {
269 &self.wallet
270 }
271}
272
273pub fn generate_mnemonic_phrase<R: Rng>(rng: &mut R, count: usize) -> Result<String> {
276 Ok(fuel_crypto::generate_mnemonic_phrase(rng, count)?)
277}
278
279#[cfg(test)]
280mod tests {
281 use tempfile::tempdir;
282
283 use super::*;
284
285 #[tokio::test]
286 async fn encrypted_json_keystore() -> Result<()> {
287 let dir = tempdir()?;
288 let mut rng = rand::thread_rng();
289
290 let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?;
292
293 let message = Message::new("Hello there!".as_bytes());
295 let signature = wallet.sign(message).await?;
296
297 let path = Path::new(dir.path()).join(uuid);
299 let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?;
300
301 let signature2 = recovered_wallet.sign(message).await?;
303 assert_eq!(signature, signature2);
304
305 assert!(std::fs::remove_file(&path).is_ok());
307 Ok(())
308 }
309
310 #[tokio::test]
311 async fn mnemonic_generation() -> Result<()> {
312 let mnemonic = generate_mnemonic_phrase(&mut rand::thread_rng(), 12)?;
313 let _wallet = WalletUnlocked::new_from_mnemonic_phrase(&mnemonic, None)?;
314
315 Ok(())
316 }
317
318 #[tokio::test]
319 async fn wallet_from_mnemonic_phrase() -> Result<()> {
320 let phrase =
321 "oblige salon price punch saddle immune slogan rare snap desert retire surprise";
322
323 let wallet =
325 WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?;
326
327 let expected_plain_address =
328 "df9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185";
329 let expected_address = "fuel1m7wsumrvtaw6d6pwtcd809627ejzhk69pggvg0cvdyg2yynqqxzseuzply";
330
331 assert_eq!(wallet.address().hash().to_string(), expected_plain_address);
332 assert_eq!(wallet.address().to_string(), expected_address);
333
334 let wallet2 =
336 WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/1'/0/0")?;
337
338 let expected_second_plain_address =
339 "261191b0164a24fd0fd51566ec5e5b0b9ba8fb2d42dc9cf7dbbd6f23d2742759";
340 let expected_second_address =
341 "fuel1ycgervqkfgj06r74z4nwchjmpwd637edgtwfea7mh4hj85n5yavszjk4cc";
342
343 assert_eq!(
344 wallet2.address().hash().to_string(),
345 expected_second_plain_address
346 );
347 assert_eq!(wallet2.address().to_string(), expected_second_address);
348
349 Ok(())
350 }
351
352 #[tokio::test]
353 async fn encrypt_and_store_wallet_from_mnemonic() -> Result<()> {
354 let dir = tempdir()?;
355
356 let phrase =
357 "oblige salon price punch saddle immune slogan rare snap desert retire surprise";
358
359 let wallet =
361 WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?;
362
363 let uuid = wallet.encrypt(&dir, "password")?;
364
365 let path = Path::new(dir.path()).join(uuid);
366
367 let recovered_wallet = WalletUnlocked::load_keystore(&path, "password", None)?;
368
369 assert_eq!(wallet.address(), recovered_wallet.address());
370
371 assert!(std::fs::remove_file(&path).is_ok());
373 Ok(())
374 }
375}