fuels_signers/
wallet.rs

1use std::{collections::HashMap, fmt, ops, path::Path};
2
3use async_trait::async_trait;
4use elliptic_curve::rand_core;
5use eth_keystore::KeystoreError;
6use fuel_core_client::client::{PaginatedResult, PaginationRequest};
7use fuel_crypto::{Message, PublicKey, SecretKey, Signature};
8use fuel_tx::{AssetId, Bytes32, ContractId, Input, Output, Receipt, TxPointer, UtxoId, Witness};
9use fuel_types::MessageId;
10use fuels_core::{
11    abi_encoder::UnresolvedBytes,
12    offsets::{base_offset, coin_predicate_data_offset, message_predicate_data_offset},
13};
14use fuels_types::{
15    bech32::{Bech32Address, Bech32ContractId, FUEL_BECH32_HRP},
16    coin::Coin,
17    constants::BASE_ASSET_ID,
18    errors::{error, Error, Result},
19    message::Message as InputMessage,
20    resource::Resource,
21    transaction::{ScriptTransaction, Transaction, TxParameters},
22    transaction_response::TransactionResponse,
23};
24use rand::{CryptoRng, Rng};
25use thiserror::Error;
26
27use crate::{
28    provider::{Provider, ResourceFilter},
29    Signer,
30};
31
32pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'";
33
34type WalletResult<T> = std::result::Result<T, WalletError>;
35
36/// A FuelVM-compatible wallet that can be used to list assets, balances and more.
37///
38/// Note that instances of the `Wallet` type only know their public address, and as a result can
39/// only perform read-only operations.
40///
41/// In order to sign messages or send transactions, a `Wallet` must first call [`Wallet::unlock`]
42/// with a valid private key to produce a [`WalletUnlocked`].
43#[derive(Clone)]
44pub struct Wallet {
45    /// The wallet's address. The wallet's address is derived
46    /// from the first 32 bytes of SHA-256 hash of the wallet's public key.
47    pub(crate) address: Bech32Address,
48    pub(crate) provider: Option<Provider>,
49}
50
51/// A `WalletUnlocked` is equivalent to a [`Wallet`] whose private key is known and stored
52/// alongside in-memory. Knowing the private key allows a `WalletUlocked` to sign operations, send
53/// transactions, and more.
54#[derive(Clone, Debug)]
55pub struct WalletUnlocked {
56    wallet: Wallet,
57    pub(crate) private_key: SecretKey,
58}
59
60#[derive(Error, Debug)]
61/// Error thrown by the Wallet module
62pub enum WalletError {
63    /// Error propagated from the hex crate.
64    #[error(transparent)]
65    Hex(#[from] hex::FromHexError),
66    /// Error propagated by parsing of a slice
67    #[error("Failed to parse slice")]
68    Parsing(#[from] std::array::TryFromSliceError),
69    #[error("No provider was setup: make sure to set_provider in your wallet!")]
70    NoProvider,
71    /// Keystore error
72    #[error(transparent)]
73    KeystoreError(#[from] KeystoreError),
74    #[error(transparent)]
75    FuelCrypto(#[from] fuel_crypto::Error),
76}
77
78impl From<WalletError> for Error {
79    fn from(e: WalletError) -> Self {
80        Error::WalletError(e.to_string())
81    }
82}
83
84impl Wallet {
85    /// Construct a Wallet from its given public address.
86    pub fn from_address(address: Bech32Address, provider: Option<Provider>) -> Self {
87        Self { address, provider }
88    }
89
90    pub fn get_provider(&self) -> WalletResult<&Provider> {
91        self.provider.as_ref().ok_or(WalletError::NoProvider)
92    }
93
94    pub fn set_provider(&mut self, provider: Provider) {
95        self.provider = Some(provider)
96    }
97
98    pub fn address(&self) -> &Bech32Address {
99        &self.address
100    }
101
102    pub async fn get_transactions(
103        &self,
104        request: PaginationRequest<String>,
105    ) -> Result<PaginatedResult<TransactionResponse, String>> {
106        Ok(self
107            .get_provider()?
108            .get_transactions_by_owner(&self.address, request)
109            .await?)
110    }
111
112    /// Returns a vector consisting of `Input::Coin`s and `Input::Message`s for the given
113    /// asset ID and amount. The `witness_index` is the position of the witness (signature)
114    /// in the transaction's list of witnesses. In the validation process, the node will
115    /// use the witness at this index to validate the coins returned by this method.
116    pub async fn get_asset_inputs_for_amount(
117        &self,
118        asset_id: AssetId,
119        amount: u64,
120        witness_index: u8,
121    ) -> Result<Vec<Input>> {
122        let filter = ResourceFilter {
123            from: self.address().clone(),
124            asset_id,
125            amount,
126            ..Default::default()
127        };
128        self.get_provider()?
129            .get_asset_inputs(filter, witness_index)
130            .await
131    }
132
133    /// Returns a vector containing the output coin and change output given an asset and amount
134    pub fn get_asset_outputs_for_amount(
135        &self,
136        to: &Bech32Address,
137        asset_id: AssetId,
138        amount: u64,
139    ) -> Vec<Output> {
140        vec![
141            Output::coin(to.into(), amount, asset_id),
142            // Note that the change will be computed by the node.
143            // Here we only have to tell the node who will own the change and its asset ID.
144            Output::change((&self.address).into(), 0, asset_id),
145        ]
146    }
147
148    /// Gets all unspent coins of asset `asset_id` owned by the wallet.
149    pub async fn get_coins(&self, asset_id: AssetId) -> Result<Vec<Coin>> {
150        Ok(self
151            .get_provider()?
152            .get_coins(&self.address, asset_id)
153            .await?)
154    }
155
156    /// Get some spendable resources (coins and messages) of asset `asset_id` owned by the wallet
157    /// that add up at least to amount `amount`. The returned coins (UTXOs) are actual coins that
158    /// can be spent. The number of UXTOs is optimized to prevent dust accumulation.
159    pub async fn get_spendable_resources(
160        &self,
161        asset_id: AssetId,
162        amount: u64,
163    ) -> Result<Vec<Resource>> {
164        let filter = ResourceFilter {
165            from: self.address().clone(),
166            asset_id,
167            amount,
168            ..Default::default()
169        };
170        self.get_provider()?
171            .get_spendable_resources(filter)
172            .await
173            .map_err(Into::into)
174    }
175
176    /// Get the balance of all spendable coins `asset_id` for address `address`. This is different
177    /// from getting coins because we are just returning a number (the sum of UTXOs amount) instead
178    /// of the UTXOs.
179    pub async fn get_asset_balance(&self, asset_id: &AssetId) -> Result<u64> {
180        self.get_provider()?
181            .get_asset_balance(&self.address, *asset_id)
182            .await
183            .map_err(Into::into)
184    }
185
186    /// Get all the spendable balances of all assets for the wallet. This is different from getting
187    /// the coins because we are only returning the sum of UTXOs coins amount and not the UTXOs
188    /// coins themselves.
189    pub async fn get_balances(&self) -> Result<HashMap<String, u64>> {
190        self.get_provider()?
191            .get_balances(&self.address)
192            .await
193            .map_err(Into::into)
194    }
195
196    pub async fn get_messages(&self) -> Result<Vec<InputMessage>> {
197        Ok(self.get_provider()?.get_messages(&self.address).await?)
198    }
199
200    /// Unlock the wallet with the given `private_key`.
201    ///
202    /// The private key will be stored in memory until `wallet.lock()` is called or until the
203    /// wallet is `drop`ped.
204    pub fn unlock(self, private_key: SecretKey) -> WalletUnlocked {
205        WalletUnlocked {
206            wallet: self,
207            private_key,
208        }
209    }
210}
211
212impl WalletUnlocked {
213    /// Lock the wallet by `drop`ping the private key from memory.
214    pub fn lock(self) -> Wallet {
215        self.wallet
216    }
217
218    // NOTE: Rather than providing a `DerefMut` implementation, we wrap the `set_provider` method
219    // directly. This is because we should not allow the user a `&mut` handle to the inner `Wallet`
220    // as this could lead to ending up with a `WalletUnlocked` in an inconsistent state (e.g. the
221    // private key doesn't match the inner wallet's public key).
222    pub fn set_provider(&mut self, provider: Provider) {
223        self.wallet.set_provider(provider)
224    }
225
226    /// Creates a new wallet with a random private key.
227    pub fn new_random(provider: Option<Provider>) -> Self {
228        let mut rng = rand::thread_rng();
229        let private_key = SecretKey::random(&mut rng);
230        Self::new_from_private_key(private_key, provider)
231    }
232
233    /// Creates a new wallet from the given private key.
234    pub fn new_from_private_key(private_key: SecretKey, provider: Option<Provider>) -> Self {
235        let public = PublicKey::from(&private_key);
236        let hashed = public.hash();
237        let address = Bech32Address::new(FUEL_BECH32_HRP, hashed);
238        Wallet::from_address(address, provider).unlock(private_key)
239    }
240
241    /// Creates a new wallet from a mnemonic phrase.
242    /// The default derivation path is used.
243    pub fn new_from_mnemonic_phrase(
244        phrase: &str,
245        provider: Option<Provider>,
246    ) -> WalletResult<Self> {
247        let path = format!("{DEFAULT_DERIVATION_PATH_PREFIX}/0'/0/0");
248        Self::new_from_mnemonic_phrase_with_path(phrase, provider, &path)
249    }
250
251    /// Creates a new wallet from a mnemonic phrase.
252    /// It takes a path to a BIP32 derivation path.
253    pub fn new_from_mnemonic_phrase_with_path(
254        phrase: &str,
255        provider: Option<Provider>,
256        path: &str,
257    ) -> WalletResult<Self> {
258        let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(phrase, path)?;
259
260        Ok(Self::new_from_private_key(secret_key, provider))
261    }
262
263    /// Creates a new wallet and stores its encrypted version in the given path.
264    pub fn new_from_keystore<P, R, S>(
265        dir: P,
266        rng: &mut R,
267        password: S,
268        provider: Option<Provider>,
269    ) -> WalletResult<(Self, String)>
270    where
271        P: AsRef<Path>,
272        R: Rng + CryptoRng + rand_core::CryptoRng,
273        S: AsRef<[u8]>,
274    {
275        let (secret, uuid) = eth_keystore::new(dir, rng, password)?;
276
277        let secret_key = unsafe { SecretKey::from_slice_unchecked(&secret) };
278
279        let wallet = Self::new_from_private_key(secret_key, provider);
280
281        Ok((wallet, uuid))
282    }
283
284    /// Encrypts the wallet's private key with the given password and saves it
285    /// to the given path.
286    pub fn encrypt<P, S>(&self, dir: P, password: S) -> WalletResult<String>
287    where
288        P: AsRef<Path>,
289        S: AsRef<[u8]>,
290    {
291        let mut rng = rand::thread_rng();
292
293        Ok(eth_keystore::encrypt_key(
294            dir,
295            &mut rng,
296            *self.private_key,
297            password,
298        )?)
299    }
300
301    /// Recreates a wallet from an encrypted JSON wallet given the provided path and password.
302    pub fn load_keystore<P, S>(
303        keypath: P,
304        password: S,
305        provider: Option<Provider>,
306    ) -> WalletResult<Self>
307    where
308        P: AsRef<Path>,
309        S: AsRef<[u8]>,
310    {
311        let secret = eth_keystore::decrypt_key(keypath, password)?;
312        let secret_key = unsafe { SecretKey::from_slice_unchecked(&secret) };
313        Ok(Self::new_from_private_key(secret_key, provider))
314    }
315
316    /// Add base asset inputs to the transaction to cover the estimated fee.
317    /// The original base asset amount cannot be calculated reliably from
318    /// the existing transaction inputs because the selected resources may exceed
319    /// the required amount to avoid dust. Therefore we require it as an argument.
320    ///
321    /// Requires contract inputs to be at the start of the transactions inputs vec
322    /// so that their indexes are retained
323    pub async fn add_fee_resources(
324        &self,
325        tx: &mut impl Transaction,
326        previous_base_amount: u64,
327        witness_index: u8,
328    ) -> Result<()> {
329        let consensus_parameters = self
330            .get_provider()?
331            .chain_info()
332            .await?
333            .consensus_parameters;
334        let transaction_fee = tx
335            .fee_checked_from_tx(&consensus_parameters)
336            .expect("Error calculating TransactionFee");
337
338        let (base_asset_inputs, remaining_inputs): (Vec<_>, Vec<_>) =
339            tx.inputs().iter().cloned().partition(|input| {
340                matches!(input, Input::MessageSigned { .. })
341                || matches!(input, Input::CoinSigned { asset_id, .. } if asset_id == &BASE_ASSET_ID)
342            });
343
344        let base_inputs_sum: u64 = base_asset_inputs
345            .iter()
346            .map(|input| input.amount().unwrap())
347            .sum();
348        // either the inputs were setup incorrectly, or the passed previous_base_amount is wrong
349        if base_inputs_sum < previous_base_amount {
350            return Err(error!(
351                WalletError,
352                "The provided base asset amount is less than the present input coins"
353            ));
354        }
355
356        let mut new_base_amount = transaction_fee.total() + previous_base_amount;
357        // If the tx doesn't consume any UTXOs, attempting to repeat it will lead to an
358        // error due to non unique tx ids (e.g. repeated contract call with configured gas cost of 0).
359        // Here we enforce a minimum amount on the base asset to avoid this
360        let is_consuming_utxos = tx
361            .inputs()
362            .iter()
363            .any(|input| !matches!(input, Input::Contract { .. }));
364        const MIN_AMOUNT: u64 = 1;
365        if !is_consuming_utxos && new_base_amount == 0 {
366            new_base_amount = MIN_AMOUNT;
367        }
368
369        let new_base_inputs = self
370            .get_asset_inputs_for_amount(BASE_ASSET_ID, new_base_amount, witness_index)
371            .await?;
372        let adjusted_inputs: Vec<_> = remaining_inputs
373            .into_iter()
374            .chain(new_base_inputs.into_iter())
375            .collect();
376        *tx.inputs_mut() = adjusted_inputs;
377
378        let is_base_change_present = tx.outputs().iter().any(|output| {
379            matches!(output, Output::Change { asset_id, .. } if asset_id == &BASE_ASSET_ID)
380        });
381        // add a change output for the base asset if it doesn't exist and there are base inputs
382        if !is_base_change_present && new_base_amount != 0 {
383            tx.outputs_mut()
384                .push(Output::change(self.address().into(), 0, BASE_ASSET_ID));
385        }
386
387        Ok(())
388    }
389
390    /// Transfer funds from this wallet to another `Address`.
391    /// Fails if amount for asset ID is larger than address's spendable coins.
392    /// Returns the transaction ID that was sent and the list of receipts.
393    pub async fn transfer(
394        &self,
395        to: &Bech32Address,
396        amount: u64,
397        asset_id: AssetId,
398        tx_parameters: TxParameters,
399    ) -> Result<(String, Vec<Receipt>)> {
400        let inputs = self
401            .get_asset_inputs_for_amount(asset_id, amount, 0)
402            .await?;
403        let outputs = self.get_asset_outputs_for_amount(to, asset_id, amount);
404
405        let mut tx = ScriptTransaction::new(inputs, outputs, tx_parameters);
406
407        // if we are not transferring the base asset, previous base amount is 0
408        if asset_id == AssetId::default() {
409            self.add_fee_resources(&mut tx, amount, 0).await?;
410        } else {
411            self.add_fee_resources(&mut tx, 0, 0).await?;
412        };
413        self.sign_transaction(&mut tx).await?;
414
415        let tx_id = tx.id().to_string();
416        let receipts = self.get_provider()?.send_transaction(&tx).await?;
417
418        Ok((tx_id, receipts))
419    }
420
421    /// Withdraws an amount of the base asset to
422    /// an address on the base chain.
423    /// Returns the transaction ID, message ID and the list of receipts.
424    pub async fn withdraw_to_base_layer(
425        &self,
426        to: &Bech32Address,
427        amount: u64,
428        tx_parameters: TxParameters,
429    ) -> Result<(String, String, Vec<Receipt>)> {
430        let inputs = self
431            .get_asset_inputs_for_amount(BASE_ASSET_ID, amount, 0)
432            .await?;
433
434        let mut tx =
435            ScriptTransaction::build_message_to_output_tx(to.into(), amount, inputs, tx_parameters);
436
437        self.add_fee_resources(&mut tx, amount, 0).await?;
438        self.sign_transaction(&mut tx).await?;
439
440        let tx_id = tx.id().to_string();
441        let receipts = self.get_provider()?.send_transaction(&tx).await?;
442
443        let message_id = WalletUnlocked::extract_message_id(&receipts)
444            .expect("MessageId could not be retrieved from tx receipts.");
445
446        Ok((tx_id, message_id.to_string(), receipts))
447    }
448
449    fn extract_message_id(receipts: &[Receipt]) -> Option<&MessageId> {
450        receipts
451            .iter()
452            .find(|r| matches!(r, Receipt::MessageOut { .. }))
453            .and_then(|m| m.message_id())
454    }
455
456    #[allow(clippy::too_many_arguments)]
457    pub async fn spend_predicate(
458        &self,
459        predicate_address: &Bech32Address,
460        code: Vec<u8>,
461        amount: u64,
462        asset_id: AssetId,
463        to: &Bech32Address,
464        predicate_data: UnresolvedBytes,
465        tx_parameters: TxParameters,
466    ) -> Result<Vec<Receipt>> {
467        let provider = self.get_provider()?;
468
469        let filter = ResourceFilter {
470            from: predicate_address.clone(),
471            amount,
472            ..Default::default()
473        };
474        let spendable_predicate_resources = provider.get_spendable_resources(filter).await?;
475
476        // input amount is: amount < input_amount < 2*amount
477        // because of "random improve" used by get_spendable_coins()
478        let input_amount: u64 = spendable_predicate_resources
479            .iter()
480            .map(|resource| resource.amount())
481            .sum();
482
483        // Iterate through the spendable resources and calculate the appropriate offsets
484        // for the coin or message predicates
485        let mut offset = base_offset(&provider.consensus_parameters().await?);
486        let inputs = spendable_predicate_resources
487            .into_iter()
488            .map(|resource| match resource {
489                Resource::Coin(coin) => {
490                    offset += coin_predicate_data_offset(code.len());
491
492                    let data = predicate_data.clone().resolve(offset as u64);
493                    offset += data.len();
494
495                    self.create_coin_predicate(coin, asset_id, code.clone(), data)
496                }
497                Resource::Message(message) => {
498                    offset += message_predicate_data_offset(message.data.len(), code.len());
499
500                    let data = predicate_data.clone().resolve(offset as u64);
501                    offset += data.len();
502
503                    self.create_message_predicate(message, code.clone(), data)
504                }
505            })
506            .collect::<Vec<_>>();
507
508        let outputs = vec![
509            Output::coin(to.into(), amount, asset_id),
510            Output::coin(predicate_address.into(), input_amount - amount, asset_id),
511        ];
512
513        let mut tx = ScriptTransaction::new(inputs, outputs, tx_parameters);
514        // we set previous base amount to 0 because it only applies to signed coins, not predicate coins
515        self.add_fee_resources(&mut tx, 0, 0).await?;
516        self.sign_transaction(&mut tx).await?;
517
518        provider.send_transaction(&tx).await
519    }
520
521    fn create_coin_predicate(
522        &self,
523        coin: Coin,
524        asset_id: AssetId,
525        code: Vec<u8>,
526        predicate_data: Vec<u8>,
527    ) -> Input {
528        Input::coin_predicate(
529            coin.utxo_id,
530            coin.owner.into(),
531            coin.amount,
532            asset_id,
533            TxPointer::default(),
534            0,
535            code,
536            predicate_data,
537        )
538    }
539
540    fn create_message_predicate(
541        &self,
542        message: InputMessage,
543        code: Vec<u8>,
544        predicate_data: Vec<u8>,
545    ) -> Input {
546        Input::message_predicate(
547            message.message_id(),
548            message.sender.into(),
549            message.recipient.into(),
550            message.amount,
551            message.nonce,
552            message.data,
553            code,
554            predicate_data,
555        )
556    }
557
558    pub async fn receive_from_predicate(
559        &self,
560        predicate_address: &Bech32Address,
561        predicate_code: Vec<u8>,
562        amount: u64,
563        asset_id: AssetId,
564        predicate_data: UnresolvedBytes,
565        tx_parameters: TxParameters,
566    ) -> Result<Vec<Receipt>> {
567        self.spend_predicate(
568            predicate_address,
569            predicate_code,
570            amount,
571            asset_id,
572            self.address(),
573            predicate_data,
574            tx_parameters,
575        )
576        .await
577    }
578
579    /// Unconditionally transfers `balance` of type `asset_id` to
580    /// the contract at `to`.
581    /// Fails if balance for `asset_id` is larger than this wallet's spendable balance.
582    /// Returns the corresponding transaction ID and the list of receipts.
583    ///
584    /// CAUTION !!!
585    ///
586    /// This will transfer coins to a contract, possibly leading
587    /// to the PERMANENT LOSS OF COINS if not used with care.
588    pub async fn force_transfer_to_contract(
589        &self,
590        to: &Bech32ContractId,
591        balance: u64,
592        asset_id: AssetId,
593        tx_parameters: TxParameters,
594    ) -> Result<(String, Vec<Receipt>)> {
595        let zeroes = Bytes32::zeroed();
596        let plain_contract_id: ContractId = to.into();
597
598        let mut inputs = vec![Input::contract(
599            UtxoId::new(zeroes, 0),
600            zeroes,
601            zeroes,
602            TxPointer::default(),
603            plain_contract_id,
604        )];
605        inputs.extend(
606            self.get_asset_inputs_for_amount(asset_id, balance, 0)
607                .await?,
608        );
609
610        let outputs = vec![
611            Output::contract(0, zeroes, zeroes),
612            Output::change((&self.address).into(), 0, asset_id),
613        ];
614
615        // Build transaction and sign it
616        let mut tx = ScriptTransaction::build_contract_transfer_tx(
617            plain_contract_id,
618            balance,
619            asset_id,
620            inputs,
621            outputs,
622            tx_parameters,
623        );
624        // if we are not transferring the base asset, previous base amount is 0
625        let base_amount = if asset_id == AssetId::default() {
626            balance
627        } else {
628            0
629        };
630        self.add_fee_resources(&mut tx, base_amount, 0).await?;
631        self.sign_transaction(&mut tx).await?;
632
633        let tx_id = tx.id();
634        let receipts = self.get_provider()?.send_transaction(&tx).await?;
635
636        Ok((tx_id.to_string(), receipts))
637    }
638}
639
640#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
641#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
642impl Signer for WalletUnlocked {
643    type Error = WalletError;
644
645    async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
646        &self,
647        message: S,
648    ) -> WalletResult<Signature> {
649        let message = Message::new(message);
650        let sig = Signature::sign(&self.private_key, &message);
651        Ok(sig)
652    }
653
654    async fn sign_transaction<T: Transaction + Send>(&self, tx: &mut T) -> WalletResult<Signature> {
655        let id = tx.id();
656
657        // Safety: `Message::from_bytes_unchecked` is unsafe because
658        // it can't guarantee that the provided bytes will be the product
659        // of a cryptographically secure hash. However, the bytes are
660        // coming from `tx.id()`, which already uses `Hasher::hash()`
661        // to hash it using a secure hash mechanism.
662        let message = unsafe { Message::from_bytes_unchecked(*id) };
663        let sig = Signature::sign(&self.private_key, &message);
664
665        let witness = vec![Witness::from(sig.as_ref())];
666
667        let witnesses: &mut Vec<Witness> = tx.witnesses_mut();
668
669        match witnesses.len() {
670            0 => *witnesses = witness,
671            _ => {
672                witnesses.extend(witness);
673            }
674        }
675
676        Ok(sig)
677    }
678
679    fn address(&self) -> &Bech32Address {
680        &self.address
681    }
682}
683
684impl fmt::Debug for Wallet {
685    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
686        f.debug_struct("Wallet")
687            .field("address", &self.address)
688            .finish()
689    }
690}
691
692impl ops::Deref for WalletUnlocked {
693    type Target = Wallet;
694    fn deref(&self) -> &Self::Target {
695        &self.wallet
696    }
697}
698
699/// Generates a random mnemonic phrase given a random number generator and the number of words to
700/// generate, `count`.
701pub fn generate_mnemonic_phrase<R: Rng>(rng: &mut R, count: usize) -> WalletResult<String> {
702    Ok(fuel_crypto::FuelMnemonic::generate_mnemonic_phrase(
703        rng, count,
704    )?)
705}
706
707#[cfg(test)]
708mod tests {
709    use tempfile::tempdir;
710
711    use super::*;
712
713    #[tokio::test]
714    async fn encrypted_json_keystore() -> Result<()> {
715        let dir = tempdir()?;
716        let mut rng = rand::thread_rng();
717
718        // Create a wallet to be stored in the keystore.
719        let (wallet, uuid) = WalletUnlocked::new_from_keystore(&dir, &mut rng, "password", None)?;
720
721        // sign a message using the above key.
722        let message = "Hello there!";
723        let signature = wallet.sign_message(message).await?;
724
725        // Read from the encrypted JSON keystore and decrypt it.
726        let path = Path::new(dir.path()).join(uuid);
727        let recovered_wallet = WalletUnlocked::load_keystore(path.clone(), "password", None)?;
728
729        // Sign the same message as before and assert that the signature is the same.
730        let signature2 = recovered_wallet.sign_message(message).await?;
731        assert_eq!(signature, signature2);
732
733        // Remove tempdir.
734        assert!(std::fs::remove_file(&path).is_ok());
735        Ok(())
736    }
737
738    #[tokio::test]
739    async fn mnemonic_generation() -> Result<()> {
740        let mnemonic = generate_mnemonic_phrase(&mut rand::thread_rng(), 12)?;
741
742        let _wallet = WalletUnlocked::new_from_mnemonic_phrase(&mnemonic, None)?;
743        Ok(())
744    }
745
746    #[tokio::test]
747    async fn wallet_from_mnemonic_phrase() -> Result<()> {
748        let phrase =
749            "oblige salon price punch saddle immune slogan rare snap desert retire surprise";
750
751        // Create first account from mnemonic phrase.
752        let wallet =
753            WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?;
754
755        let expected_plain_address =
756            "df9d0e6c6c5f5da6e82e5e1a77974af6642bdb450a10c43f0c6910a212600185";
757        let expected_address = "fuel1m7wsumrvtaw6d6pwtcd809627ejzhk69pggvg0cvdyg2yynqqxzseuzply";
758
759        assert_eq!(wallet.address().hash().to_string(), expected_plain_address);
760        assert_eq!(wallet.address().to_string(), expected_address);
761
762        // Create a second account from the same phrase.
763        let wallet2 =
764            WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/1'/0/0")?;
765
766        let expected_second_plain_address =
767            "261191b0164a24fd0fd51566ec5e5b0b9ba8fb2d42dc9cf7dbbd6f23d2742759";
768        let expected_second_address =
769            "fuel1ycgervqkfgj06r74z4nwchjmpwd637edgtwfea7mh4hj85n5yavszjk4cc";
770
771        assert_eq!(
772            wallet2.address().hash().to_string(),
773            expected_second_plain_address
774        );
775        assert_eq!(wallet2.address().to_string(), expected_second_address);
776
777        Ok(())
778    }
779
780    #[tokio::test]
781    async fn encrypt_and_store_wallet_from_mnemonic() -> Result<()> {
782        let dir = tempdir()?;
783
784        let phrase =
785            "oblige salon price punch saddle immune slogan rare snap desert retire surprise";
786
787        // Create first account from mnemonic phrase.
788        let wallet =
789            WalletUnlocked::new_from_mnemonic_phrase_with_path(phrase, None, "m/44'/60'/0'/0/0")?;
790
791        let uuid = wallet.encrypt(&dir, "password")?;
792
793        let path = Path::new(dir.path()).join(uuid);
794
795        let recovered_wallet = WalletUnlocked::load_keystore(&path, "password", None)?;
796
797        assert_eq!(wallet.address(), recovered_wallet.address());
798
799        // Remove tempdir.
800        assert!(std::fs::remove_file(&path).is_ok());
801        Ok(())
802    }
803}