fuels_accounts/
wallet.rs

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/// A FuelVM-compatible wallet that can be used to list assets, balances and more.
25///
26/// Note that instances of the `Wallet` type only know their public address, and as a result can
27/// only perform read-only operations.
28///
29/// In order to sign messages or send transactions, a `Wallet` must first call [`Wallet::unlock`]
30/// with a valid private key to produce a [`WalletUnlocked`].
31#[derive(Clone)]
32pub struct Wallet {
33    /// The wallet's address. The wallet's address is derived
34    /// from the first 32 bytes of SHA-256 hash of the wallet's public key.
35    pub(crate) address: Bech32Address,
36    provider: Option<Provider>,
37}
38
39/// A `WalletUnlocked` is equivalent to a [`Wallet`] whose private key is known and stored
40/// alongside in-memory. Knowing the private key allows a `WalletUlocked` to sign operations, send
41/// transactions, and more.
42///
43/// `private_key` will be zeroed out on calling `lock()` or `drop`ping a `WalletUnlocked`.
44#[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    /// Construct a Wallet from its given public address.
53    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    /// Unlock the wallet with the given `private_key`.
70    ///
71    /// The private key will be stored in memory until `wallet.lock()` is called or until the
72    /// wallet is `drop`ped.
73    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    /// Lock the wallet by securely `zeroize`-ing and `drop`ping the private key from memory.
108    pub fn lock(mut self) -> Wallet {
109        self.private_key.zeroize();
110        self.wallet.clone()
111    }
112
113    // NOTE: Rather than providing a `DerefMut` implementation, we wrap the `set_provider` method
114    // directly. This is because we should not allow the user a `&mut` handle to the inner `Wallet`
115    // as this could lead to ending up with a `WalletUnlocked` in an inconsistent state (e.g. the
116    // private key doesn't match the inner wallet's public key).
117    pub fn set_provider(&mut self, provider: Provider) {
118        self.wallet.set_provider(provider);
119    }
120
121    /// Creates a new wallet with a random private key.
122    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    /// Creates a new wallet from the given private key.
129    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    /// Creates a new wallet from a mnemonic phrase.
137    /// The default derivation path is used.
138    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    /// Creates a new wallet from a mnemonic phrase.
144    /// It takes a derivation path such as BIP32 or BIP44.
145    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    /// Creates a new wallet and stores its encrypted version in the given path.
156    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    /// Encrypts the wallet's private key with the given password and saves it
178    /// to the given path.
179    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    /// Recreates a wallet from an encrypted JSON wallet given the provided path and password.
191    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    /// Returns the private key of the wallet. This method is only available when the 'test-helpers' feature is enabled.
208    #[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
273/// Generates a random mnemonic phrase given a random number generator and the number of words to
274/// generate, `count`.
275pub 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        // Create a wallet to be stored in the keystore.
291        let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?;
292
293        // sign a message using the above key.
294        let message = Message::new("Hello there!".as_bytes());
295        let signature = wallet.sign(message).await?;
296
297        // Read from the encrypted JSON keystore and decrypt it.
298        let path = Path::new(dir.path()).join(uuid);
299        let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?;
300
301        // Sign the same message as before and assert that the signature is the same.
302        let signature2 = recovered_wallet.sign(message).await?;
303        assert_eq!(signature, signature2);
304
305        // Remove tempdir.
306        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        // Create first account from mnemonic phrase.
324        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        // Create a second account from the same phrase.
335        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        // Create first account from mnemonic phrase.
360        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        // Remove tempdir.
372        assert!(std::fs::remove_file(&path).is_ok());
373        Ok(())
374    }
375}