forc_wallet/
account.rs

1use crate::format::Table;
2use crate::sign;
3use crate::utils::{
4    display_string_discreetly, get_derivation_path, load_wallet, user_fuel_wallets_accounts_dir,
5};
6use anyhow::{anyhow, bail, Context, Result};
7use clap::{Args, Subcommand};
8use eth_keystore::EthKeystore;
9use forc_tracing::println_warning;
10use fuels::accounts::provider::Provider;
11use fuels::accounts::signers::private_key::PrivateKeySigner;
12use fuels::accounts::wallet::Unlocked;
13use fuels::accounts::ViewOnlyAccount;
14use fuels::crypto::{PublicKey, SecretKey};
15use fuels::types::checksum_address::{checksum_encode, is_checksum_valid};
16use fuels::types::transaction::TxPolicies;
17use fuels::types::{Address, AssetId};
18use fuels::{
19    accounts::wallet::Wallet,
20    types::bech32::{Bech32Address, FUEL_BECH32_HRP},
21};
22use std::ops::Range;
23use std::{
24    collections::BTreeMap,
25    fmt, fs,
26    path::{Path, PathBuf},
27    str::FromStr,
28};
29use url::Url;
30
31type WalletUnlocked<S> = Wallet<Unlocked<S>>;
32
33#[derive(Debug, Args)]
34pub struct Accounts {
35    #[clap(flatten)]
36    unverified: UnverifiedOpt,
37    /// Contains optional flag for displaying all accounts as hex / bytes values.
38    ///
39    /// pass in --as-hex for this alternative display.
40    #[clap(long)]
41    as_bech32: bool,
42}
43
44#[derive(Debug, Args)]
45pub struct Account {
46    /// The index of the account.
47    ///
48    /// This index is used directly within the path used to derive the account.
49    index: Option<usize>,
50    #[clap(flatten)]
51    unverified: UnverifiedOpt,
52    #[clap(subcommand)]
53    cmd: Option<Command>,
54}
55
56#[derive(Debug, Args)]
57pub(crate) struct Fmt {
58    /// Option for public key to be displayed as hex / bytes.
59    ///
60    /// pass in --as-hex for this alternative display.
61    #[clap(long)]
62    as_hex: bool,
63}
64
65#[derive(Debug, Subcommand)]
66pub(crate) enum Command {
67    /// Derive and reveal a new account for the wallet.
68    ///
69    /// Note that upon derivation of the new account, the account's public
70    /// address will be cached in plain text for convenient retrieval via the
71    /// `accounts` and `account <ix>` commands.
72    ///
73    /// The index of the newly derived account will be that which succeeds the
74    /// greatest known account index currently within the cache.
75    New,
76    /// Sign a transaction with the specified account.
77    #[clap(subcommand)]
78    Sign(sign::Data),
79    /// Temporarily display the private key of an account from its index.
80    ///
81    /// WARNING: This prints your account's private key to an alternative,
82    /// temporary, terminal window!
83    PrivateKey,
84    /// Reveal the public key for the specified account.
85    /// Takes an optional bool flag --as-hex that displays the PublicKey in hex format.
86    PublicKey(Fmt),
87    /// Print each asset balance associated with the specified account.
88    Balance(Balance),
89    /// Transfer assets from this account to another.
90    Transfer(Transfer),
91}
92
93#[derive(Debug, Args)]
94pub(crate) struct Balance {
95    #[clap(flatten)]
96    pub(crate) unverified: UnverifiedOpt,
97}
98
99#[derive(Debug, Args)]
100pub(crate) struct Transfer {
101    /// The address (in bech32 or hex) of the account to transfer assets to.
102    #[clap(long)]
103    to: To,
104    /// Amount (in u64) of assets to transfer.
105    #[clap(long)]
106    amount: u64,
107    /// Asset ID of the asset to transfer.
108    #[clap(long)]
109    asset_id: AssetId,
110    #[clap(long)]
111    gas_price: Option<u64>,
112    #[clap(long)]
113    gas_limit: Option<u64>,
114    #[clap(long)]
115    maturity: Option<u64>,
116}
117
118#[derive(Debug, Args)]
119pub(crate) struct UnverifiedOpt {
120    /// When enabled, shows account addresses stored in the cache without re-deriving them.
121    ///
122    /// The cache can be found at `~/.fuel/wallets/addresses`.
123    ///
124    /// Useful for non-interactive scripts on trusted systems or integration tests.
125    #[clap(long = "unverified")]
126    pub(crate) unverified: bool,
127}
128
129#[derive(Debug, Clone)]
130enum To {
131    Bech32Address(Bech32Address),
132    HexAddress(Address),
133}
134
135impl FromStr for To {
136    type Err = String;
137
138    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
139        if let Ok(bech32_address) = Bech32Address::from_str(s) {
140            return Ok(Self::Bech32Address(bech32_address));
141        } else if let Ok(hex_address) = Address::from_str(s) {
142            if !is_checksum_valid(s) {
143                return Err(format!(
144                    "Checksum is not valid for address `{}`, the address might not be an account.",
145                    s
146                ));
147            }
148            return Ok(Self::HexAddress(hex_address));
149        }
150
151        Err(format!(
152            "Invalid address '{}': address must either be in bech32 or hex",
153            s
154        ))
155    }
156}
157
158impl fmt::Display for To {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            To::Bech32Address(bech32_addr) => write!(f, "{bech32_addr}"),
162            To::HexAddress(hex_addr) => {
163                // This `unwrap` is fine, because only way to create a `To` is
164                // `from_str` and providing an invalid checksum, or non-hex
165                // addr is already handled by that routine.
166                let hex_addr = checksum_encode(&format!("0x{hex_addr}")).unwrap();
167                write!(f, "{hex_addr}")
168            }
169        }
170    }
171}
172
173/// A map from an account's index to its bech32 address.
174type AccountAddresses = BTreeMap<usize, Address>;
175
176pub async fn cli(ctx: &crate::CliContext, account: Account) -> Result<()> {
177    match (account.index, account.cmd) {
178        (None, Some(Command::New)) => new_cli(ctx).await?,
179        (Some(acc_ix), Some(Command::New)) => new_at_index_cli(ctx, acc_ix).await?,
180        (Some(acc_ix), None) => print_address(ctx, acc_ix, account.unverified.unverified).await?,
181        (Some(acc_ix), Some(Command::Sign(sign_cmd))) => {
182            sign::wallet_account_cli(ctx, acc_ix, sign_cmd)?
183        }
184        (Some(acc_ix), Some(Command::PrivateKey)) => private_key_cli(ctx, acc_ix)?,
185        (Some(acc_ix), Some(Command::PublicKey(format))) => match format.as_hex {
186            true => hex_address_cli(ctx, acc_ix)?,
187            false => public_key_cli(ctx, acc_ix)?,
188        },
189
190        (Some(acc_ix), Some(Command::Balance(balance))) => {
191            account_balance_cli(ctx, acc_ix, &balance).await?
192        }
193        (Some(acc_ix), Some(Command::Transfer(transfer))) => {
194            transfer_cli(ctx, acc_ix, transfer).await?
195        }
196        (None, Some(cmd)) => print_subcmd_index_warning(&cmd),
197        (None, None) => print_subcmd_help(),
198    }
199    Ok(())
200}
201
202pub(crate) async fn account_balance_cli(
203    ctx: &crate::CliContext,
204    acc_ix: usize,
205    balance: &Balance,
206) -> Result<()> {
207    let wallet = load_wallet(&ctx.wallet_path)?;
208    let provider = Provider::connect(&ctx.node_url).await?;
209    let mut cached_addrs = read_cached_addresses(&wallet.crypto.ciphertext)?;
210    let cached_addr = cached_addrs
211        .remove(&acc_ix)
212        .ok_or_else(|| anyhow!("No cached address for account {acc_ix}"))?;
213
214    let account = if balance.unverified.unverified {
215        let cached_addr = Bech32Address::from(cached_addr);
216        Wallet::new_locked(cached_addr, provider)
217    } else {
218        let prompt = format!("Please enter your wallet password to verify account {acc_ix}: ");
219        let password = rpassword::prompt_password(prompt)?;
220        let account = derive_account_unlocked(&ctx.wallet_path, acc_ix, &password, &provider)?;
221        let cached_addr = Bech32Address::from(cached_addr);
222        verify_address_and_update_cache(acc_ix, &account, &cached_addr, &wallet.crypto.ciphertext)?;
223        account.lock()
224    };
225    println!("Connecting to {}", &ctx.node_url);
226    println!("Fetching the balance of the following account:",);
227    let account_adr = checksum_encode(&format!("0x{}", account.address()))?;
228    println!("  {acc_ix:>3}: {}", account_adr);
229    let account_balance: BTreeMap<_, _> = account.get_balances().await?.into_iter().collect();
230    println!("\nAccount {acc_ix}:");
231    if account_balance.is_empty() {
232        print_balance_empty(&ctx.node_url);
233    } else {
234        print_balance(&account_balance);
235    }
236    Ok(())
237}
238
239/// Display a warning to the user if the expected address differs from the account address.
240/// Returns `Ok(true)` if the address matched, `Ok(false)` if it did not, `Err` if we failed to
241/// update the cache.
242pub(crate) fn verify_address_and_update_cache(
243    acc_ix: usize,
244    account: &Wallet,
245    expected_addr: &Bech32Address,
246    wallet_ciphertext: &[u8],
247) -> Result<bool> {
248    let addr = account.address();
249    if addr == expected_addr {
250        return Ok(true);
251    }
252    println_warning(&format!(
253        "Cached address for account {} differs from derived address.\n\
254{:>2}Cached: {}
255{:>2}Derived: {}
256{:>2}Updating cache with newly derived address.",
257        acc_ix, "", expected_addr, "", addr, "",
258    ));
259    cache_address(wallet_ciphertext, acc_ix, addr)?;
260    Ok(false)
261}
262
263pub(crate) fn print_balance_empty(node_url: &Url) {
264    let testnet_url = crate::network::TESTNET.parse::<Url>().unwrap();
265
266    let faucet_url = match node_url.host_str() {
267        host if host == testnet_url.host_str() => crate::network::TESTNET_FAUCET,
268        _ => return println!("  Account empty."),
269    };
270    if node_url
271        .host_str()
272        .is_some_and(|a| a == crate::network::MAINNET)
273    {
274        println!("  Account empty.");
275    } else {
276        println!(
277            "  Account empty. Visit the faucet to acquire some test funds: {}",
278            faucet_url
279        );
280    }
281}
282
283pub(crate) fn print_balance(balance: &BTreeMap<String, u128>) {
284    let mut table = Table::default();
285    table.add_header("Asset ID");
286    table.add_header("Amount");
287
288    for (asset_id, amount) in balance {
289        table
290            .add_row(vec![asset_id.to_owned(), amount.to_string()])
291            .expect("add_row");
292    }
293    println!("{}", table);
294}
295
296/// Prints a list of all known (cached) accounts for the wallet at the given path.
297pub async fn print_accounts_cli(ctx: &crate::CliContext, accounts: Accounts) -> Result<()> {
298    let wallet = load_wallet(&ctx.wallet_path)?;
299    let addresses = read_cached_addresses(&wallet.crypto.ciphertext)?;
300    if accounts.unverified.unverified {
301        println!("Account addresses (unverified, printed from cache):");
302        addresses
303            .iter()
304            .for_each(|(ix, addr)| match accounts.as_bech32 {
305                false => {
306                    println!("[{ix}] {addr}")
307                }
308                true => {
309                    let bytes_addr: Bech32Address = Bech32Address::from(*addr);
310                    println!("[{ix}] {bytes_addr}");
311                }
312            });
313    } else {
314        let prompt = "Please enter your wallet password to verify cached accounts: ";
315        let password = rpassword::prompt_password(prompt)?;
316        let provider = Provider::connect(&ctx.node_url).await?;
317        for &ix in addresses.keys() {
318            let account = derive_account_unlocked(&ctx.wallet_path, ix, &password, &provider)?;
319            let account_addr = account.address();
320            match accounts.as_bech32 {
321                false => {
322                    let account_addr: Address = account.address().into();
323                    let account_addr = checksum_encode(&format!("0x{account_addr}"))?;
324                    println!("[{ix}] {account_addr}")
325                }
326                true => {
327                    let bytes_addr: Bech32Address = Bech32Address::from(account_addr);
328                    println!("[{ix}] {bytes_addr}");
329                }
330            }
331
332            cache_address(&wallet.crypto.ciphertext, ix, account_addr)?;
333        }
334    }
335    Ok(())
336}
337
338fn print_subcmd_help() {
339    // The user must provide either the account index or a `New`
340    // command - otherwise we print the help output for the
341    // `account` subcommand. There doesn't seem to be a nice way
342    // of doing this with clap's derive API, so we do-so with a
343    // child process.
344    std::process::Command::new("forc-wallet")
345        .args(["account", "--help"])
346        .stdout(std::process::Stdio::inherit())
347        .stderr(std::process::Stdio::inherit())
348        .output()
349        .expect("failed to invoke `forc wallet account --help` command");
350}
351
352fn print_subcmd_index_warning(cmd: &Command) {
353    let cmd_str = match cmd {
354        Command::Sign(_) => "sign",
355        Command::PrivateKey => "private-key",
356        Command::PublicKey(_) => "public-key",
357        Command::Transfer(_) => "transfer",
358        Command::Balance(_) => "balance",
359        Command::New => unreachable!("new is valid without an index"),
360    };
361    eprintln!(
362        "Error: The command `{cmd_str}` requires an account index. \
363        For example: `forc wallet account <INDEX> {cmd_str} ...`\n"
364    );
365    print_subcmd_help();
366}
367
368/// Print the address of the wallet's account at the given index.
369pub async fn print_address(
370    ctx: &crate::CliContext,
371    account_ix: usize,
372    unverified: bool,
373) -> Result<()> {
374    let wallet = load_wallet(&ctx.wallet_path)?;
375    if unverified {
376        let addresses = read_cached_addresses(&wallet.crypto.ciphertext)?;
377        match addresses.get(&account_ix) {
378            Some(address) => println!("Account {account_ix} address (unverified): {address}"),
379            None => eprintln!("Account {account_ix} is not derived yet!"),
380        }
381    } else {
382        let prompt = format!("Please enter your wallet password to verify account {account_ix}: ");
383        let password = rpassword::prompt_password(prompt)?;
384        let provider = Provider::connect(&ctx.node_url).await?;
385        let account = derive_account_unlocked(&ctx.wallet_path, account_ix, &password, &provider)?;
386        let account_addr = account.address();
387        let checksum_addr = checksum_encode(&format!("0x{}", Address::from(account_addr)))?;
388        println!("Account {account_ix} address: {checksum_addr}");
389        cache_address(&wallet.crypto.ciphertext, account_ix, account_addr)?;
390    }
391    Ok(())
392}
393
394/// Given a path to a wallet, an account index and the wallet's password,
395/// derive the account address for the account at the given index.
396pub fn derive_secret_key(
397    wallet_path: &Path,
398    account_index: usize,
399    password: &str,
400) -> Result<SecretKey> {
401    let phrase_recovered = eth_keystore::decrypt_key(wallet_path, password)?;
402    let phrase = String::from_utf8(phrase_recovered)?;
403    let derive_path = get_derivation_path(account_index);
404    let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(&phrase, &derive_path)?;
405    Ok(secret_key)
406}
407
408fn next_derivation_index(addrs: &AccountAddresses) -> usize {
409    addrs.last_key_value().map(|(&ix, _)| ix + 1).unwrap_or(0)
410}
411
412/// Derive an account at the first index succeeding the greatest known existing index.
413pub(crate) fn derive_account_unlocked(
414    wallet_path: &Path,
415    account_ix: usize,
416    password: &str,
417    provider: &Provider,
418) -> Result<WalletUnlocked<PrivateKeySigner>> {
419    let secret_key = derive_secret_key(wallet_path, account_ix, password)?;
420    let wallet = WalletUnlocked::new(PrivateKeySigner::new(secret_key), provider.clone());
421    Ok(wallet)
422}
423
424pub async fn derive_and_cache_addresses(
425    ctx: &crate::CliContext,
426    mnemonic: &str,
427    range: Range<usize>,
428) -> anyhow::Result<BTreeMap<usize, Address>> {
429    let wallet = load_wallet(&ctx.wallet_path)?;
430    let provider = Provider::connect(&ctx.node_url).await?;
431    range
432        .into_iter()
433        .map(|acc_ix| {
434            let derive_path = get_derivation_path(acc_ix);
435            let secret_key = SecretKey::new_from_mnemonic_phrase_with_path(mnemonic, &derive_path)?;
436            let account = WalletUnlocked::new(PrivateKeySigner::new(secret_key), provider.clone());
437            cache_address(&wallet.crypto.ciphertext, acc_ix, account.address())?;
438
439            Ok(account.address().to_owned().into())
440        })
441        .collect::<Result<Vec<_>, _>>()
442        .map(|x| x.into_iter().enumerate().collect())
443}
444
445fn new_at_index(
446    keystore: &EthKeystore,
447    wallet_path: &Path,
448    account_ix: usize,
449    provider: &Provider,
450) -> Result<String> {
451    let prompt = format!("Please enter your wallet password to derive account {account_ix}: ");
452    let password = rpassword::prompt_password(prompt)?;
453    let account = derive_account_unlocked(wallet_path, account_ix, &password, provider)?;
454    let account_addr = account.address();
455    cache_address(&keystore.crypto.ciphertext, account_ix, account_addr)?;
456    let checksum_addr = checksum_encode(&Address::from(account_addr).to_string())?;
457    println!("Wallet address: {checksum_addr}");
458    Ok(checksum_addr)
459}
460
461pub async fn new_at_index_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
462    let keystore = load_wallet(&ctx.wallet_path)?;
463    let provider = Provider::connect(&ctx.node_url).await?;
464    new_at_index(&keystore, &ctx.wallet_path, account_ix, &provider)?;
465    Ok(())
466}
467
468pub(crate) async fn new_cli(ctx: &crate::CliContext) -> Result<()> {
469    let keystore = load_wallet(&ctx.wallet_path)?;
470    let addresses = read_cached_addresses(&keystore.crypto.ciphertext)?;
471    let account_ix = next_derivation_index(&addresses);
472    let provider = Provider::connect(&ctx.node_url).await?;
473    new_at_index(&keystore, &ctx.wallet_path, account_ix, &provider)?;
474    Ok(())
475}
476
477pub(crate) fn private_key_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
478    let prompt = format!(
479        "Please enter your wallet password to display account {account_ix}'s private key: "
480    );
481    let password = rpassword::prompt_password(prompt)?;
482    let secret_key = derive_secret_key(&ctx.wallet_path, account_ix, &password)?;
483    let secret_key_string = format!("Secret key for account {account_ix}: {secret_key}\n");
484    display_string_discreetly(&secret_key_string, "### Press any key to complete. ###")?;
485    Ok(())
486}
487
488/// Prints the public key of given account index.
489pub(crate) fn public_key_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
490    let prompt =
491        format!("Please enter your wallet password to display account {account_ix}'s public key: ");
492    let password = rpassword::prompt_password(prompt)?;
493    let secret_key = derive_secret_key(&ctx.wallet_path, account_ix, &password)?;
494    let public_key = PublicKey::from(&secret_key);
495    println!("Public key for account {account_ix}: {public_key}");
496    Ok(())
497}
498
499/// Prints the plain address for the given account index
500pub(crate) fn hex_address_cli(ctx: &crate::CliContext, account_ix: usize) -> Result<()> {
501    let prompt = format!(
502        "Please enter your wallet password to display account {account_ix}'s plain address: "
503    );
504    let password = rpassword::prompt_password(prompt)?;
505    let secret_key = derive_secret_key(&ctx.wallet_path, account_ix, &password)?;
506    let public_key = PublicKey::from(&secret_key);
507    let hashed = public_key.hash();
508    let bech = Bech32Address::new(FUEL_BECH32_HRP, hashed);
509    let plain_address: Address = bech.into();
510    println!("Plain address for {}: {}", account_ix, plain_address);
511    Ok(())
512}
513
514/// Transfers assets from account at a given account index to a target address.
515pub(crate) async fn transfer_cli(
516    ctx: &crate::CliContext,
517    acc_ix: usize,
518    transfer: Transfer,
519) -> Result<()> {
520    use fuels::accounts::Account;
521
522    println!(
523        "Preparing to transfer:\n  Amount: {}\n  Asset ID: 0x{}\n  To: {}\n",
524        transfer.amount, transfer.asset_id, transfer.to
525    );
526    let provider = Provider::connect(&ctx.node_url).await?;
527
528    let to = match transfer.to {
529        To::Bech32Address(bech32_addr) => bech32_addr,
530        To::HexAddress(hex_addr) => {
531            // Check if `to` is an account, we know that checksum is valid at
532            // this point. Otherwise, `To` won't even parse from user input.
533            // At this point we want to query the provider to see if the
534            // acount is actually something we can transfer to.
535            let addr = checksum_encode(&format!("0x{hex_addr}"))?;
536            let to_addr = fuels::types::Bytes32::from_str(&addr).map_err(|e| anyhow!("{e}"))?;
537            if !provider.is_user_account(to_addr).await? {
538                bail!(format!("{addr} is not a user account. Aborting transfer."))
539            }
540            Bech32Address::from(hex_addr)
541        }
542    };
543
544    let prompt = format!(
545        "Please enter your wallet password to unlock account {acc_ix} and to initiate transfer: "
546    );
547    let password = rpassword::prompt_password(prompt)?;
548    let mut account = derive_account_unlocked(&ctx.wallet_path, acc_ix, &password, &provider)?;
549    account.set_provider(provider);
550    println!("Transferring...");
551
552    let tx_response = account
553        .transfer(
554            &to,
555            transfer.amount,
556            transfer.asset_id,
557            TxPolicies::new(
558                transfer.gas_price,
559                None,
560                transfer.maturity,
561                None,
562                None,
563                transfer.gas_limit,
564            ),
565        )
566        .await?;
567
568    let block_explorer_url = match ctx.node_url.host_str() {
569        host if host == crate::network::MAINNET.parse::<Url>().unwrap().host_str() => {
570            crate::explorer::DEFAULT
571        }
572        host if host == crate::network::TESTNET.parse::<Url>().unwrap().host_str() => {
573            crate::explorer::TESTNET
574        }
575        _ => "",
576    };
577
578    let tx_explorer_url = format!("{block_explorer_url}/tx/0x{}", tx_response.tx_id);
579    println!(
580        "\nTransfer complete!\nSummary:\n  Transaction ID: 0x{}\n  Receipts: {:#?}\n  Explorer: {}\n",
581        tx_response.tx_id,
582        tx_response.tx_status.receipts,
583        tx_explorer_url
584    );
585
586    Ok(())
587}
588
589/// A unique 64-bit hash is created from the wallet's ciphertext to use as a unique directory name.
590fn address_cache_dir_name(wallet_ciphertext: &[u8]) -> String {
591    use std::hash::{Hash, Hasher};
592    let hasher = &mut std::collections::hash_map::DefaultHasher::default();
593    wallet_ciphertext.iter().for_each(|byte| byte.hash(hasher));
594    let hash = hasher.finish();
595    format!("{hash:x}")
596}
597
598/// The path in which a wallet's account addresses will be cached.
599fn address_cache_dir(wallet_ciphertext: &[u8]) -> PathBuf {
600    user_fuel_wallets_accounts_dir().join(address_cache_dir_name(wallet_ciphertext))
601}
602
603/// The cache path for a wallet account address.
604fn address_path(wallet_ciphertext: &[u8], account_ix: usize) -> PathBuf {
605    address_cache_dir(wallet_ciphertext).join(format!("{account_ix}"))
606}
607
608/// Cache a single wallet account address to a file as a simple utf8 string.
609pub fn cache_address(
610    wallet_ciphertext: &[u8],
611    account_ix: usize,
612    account_addr: &Bech32Address,
613) -> Result<()> {
614    let path = address_path(wallet_ciphertext, account_ix);
615    if path.exists() && !path.is_file() {
616        bail!("attempting to cache account address to {path:?}, but the path is a directory");
617    }
618    let parent = path
619        .parent()
620        .expect("account address path contained no parent directory");
621    fs::create_dir_all(parent).context("failed to create account address cache directory")?;
622    fs::write(path, account_addr.to_string()).context("failed to cache account address to file")?;
623    Ok(())
624}
625
626/// Read all cached account addresses for the wallet with the given ciphertext.
627pub(crate) fn read_cached_addresses(wallet_ciphertext: &[u8]) -> Result<AccountAddresses> {
628    let wallet_accounts_dir = address_cache_dir(wallet_ciphertext);
629    if !wallet_accounts_dir.exists() {
630        return Ok(Default::default());
631    }
632    fs::read_dir(&wallet_accounts_dir)
633        .context("failed to read account address cache")?
634        .map(|res| {
635            let entry = res.context("failed to read account address cache")?;
636            let path = entry.path();
637            let file_name = path
638                .file_name()
639                .and_then(|os_str| os_str.to_str())
640                .ok_or_else(|| anyhow!("failed to read utf8 file name from {path:?}"))?;
641            let account_ix: usize = file_name
642                .parse()
643                .context("failed to parse account index from file name")?;
644            let account_addr_str = std::fs::read_to_string(&path)
645                .context("failed to read account address from cache")?;
646            let account_addr_bech32: Bech32Address = account_addr_str
647                .parse()
648                .context("failed to parse cached account address as a bech32 address")?;
649            let account_addr: Address = account_addr_bech32.into();
650            Ok((account_ix, account_addr))
651        })
652        .collect()
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658    use crate::utils::test_utils::{
659        mock_provider, with_tmp_dir_and_wallet, TEST_MNEMONIC, TEST_PASSWORD,
660    };
661    use crate::utils::write_wallet_from_mnemonic_and_password;
662    use fuels::types::Address;
663
664    #[tokio::test]
665    async fn create_new_account() {
666        let mock_provider = mock_provider().await;
667
668        let tmp_dir = tempfile::TempDir::new().unwrap();
669        let wallet_path = tmp_dir.path().join("wallet.json");
670        write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
671            .unwrap();
672
673        let wallet = derive_account_unlocked(&wallet_path, 0, TEST_PASSWORD, &mock_provider)
674            .expect("wallet unlocked");
675        let wallet_addr = wallet.address();
676        let wallet_addr_str = wallet_addr.to_string();
677        assert_eq!(
678            wallet_addr_str,
679            "fuel1j9zsg4yt45adrcky3xlr4a5rah5ync5xhms2xjtyfm0teyfx000q94t6el"
680        );
681        let wallet_hash = wallet_addr.hash();
682        assert_eq!(
683            wallet_hash.to_string(),
684            "914504548bad3ad1e2c489be3af683ede849e286bee0a349644edebc91267bde"
685        );
686    }
687
688    #[test]
689    fn derive_account_by_index() {
690        with_tmp_dir_and_wallet(|_dir, wallet_path| {
691            // derive account with account index 0
692            let account_ix = 0;
693            let private_key = derive_secret_key(wallet_path, account_ix, TEST_PASSWORD).unwrap();
694            assert_eq!(
695                private_key.to_string(),
696                "961bf9754dd036dd13b1d543b3c0f74062bc4ac668ea89d38ce8d712c591f5cf"
697            )
698        });
699    }
700    #[test]
701    fn derive_plain_address() {
702        let address = "fuel1j78es08cyyz5n75jugal7p759ccs323etnykzpndsvhzu6399yqqpjmmd2";
703        let bech32 = <fuels::types::bech32::Bech32Address as std::str::FromStr>::from_str(address)
704            .expect("failed to create Bech32 address from string");
705        let plain_address: Address = bech32.into();
706        assert_eq!(
707            <Address as std::str::FromStr>::from_str(
708                "978f983cf8210549fa92e23bff07d42e3108aa395cc961066d832e2e6a252900"
709            )
710            .expect("RIP"),
711            plain_address
712        )
713    }
714}