forc_wallet/
balance.rs

1use crate::{
2    DEFAULT_CACHE_ACCOUNTS,
3    account::{
4        derive_account_unlocked, derive_and_cache_addresses, print_balance, print_balance_empty,
5        read_cached_addresses, verify_address_and_update_cache,
6    },
7    format::List,
8    utils::load_wallet,
9};
10use anyhow::{Result, anyhow};
11use clap::Args;
12use fuels::{
13    accounts::{ViewOnlyAccount, provider::Provider, wallet::Wallet},
14    types::{Address, checksum_address::checksum_encode},
15};
16use std::{
17    cmp::max,
18    collections::{BTreeMap, HashMap},
19    path::Path,
20};
21use url::Url;
22
23#[derive(Debug, Args)]
24#[group(skip)]
25pub struct Balance {
26    // Account-specific args.
27    #[clap(flatten)]
28    pub(crate) account: crate::account::Balance,
29    /// Show the balance for each individual non-empty account before showing
30    /// the total.
31    #[clap(long)]
32    pub(crate) accounts: bool,
33}
34
35/// Whether to verify cached accounts or not.
36///
37/// To verify cached accounts we require wallet vault password.
38pub enum AccountVerification {
39    No,
40    Yes(String),
41}
42
43/// List of accounts and amount of tokens they hold with different ASSET_IDs.
44pub type AccountBalances = Vec<HashMap<String, u128>>;
45/// A mapping between account index and the address for that account.
46pub type AccountsMap = BTreeMap<usize, Address>;
47
48/// Return a map of accounts after desired verification applied in a map where each key is account
49/// index and each value is the `Address` of that account.
50pub async fn collect_accounts_with_verification(
51    wallet_path: &Path,
52    verification: AccountVerification,
53    node_url: &Url,
54) -> Result<AccountsMap> {
55    let wallet = load_wallet(wallet_path)?;
56    let mut addresses = read_cached_addresses(&wallet.crypto.ciphertext)?;
57    if let AccountVerification::Yes(password) = verification {
58        for (&ix, addr) in addresses.iter_mut() {
59            let provider = Provider::connect(node_url).await?;
60            let account = derive_account_unlocked(wallet_path, ix, &password, &provider)?;
61            if verify_address_and_update_cache(ix, &account, addr, &wallet.crypto.ciphertext)? {
62                *addr = account.address();
63            }
64        }
65    }
66
67    Ok(addresses)
68}
69
70/// Returns N derived addresses. If the `unverified` flag is set, it will not verify the addresses
71/// and will use the cached ones.
72///
73/// This function will override / fix the cached addresses if the user password is requested
74pub async fn get_derived_accounts(
75    ctx: &crate::CliContext,
76    unverified: bool,
77    target_accounts: Option<usize>,
78) -> Result<AccountsMap> {
79    let wallet = load_wallet(&ctx.wallet_path)?;
80    let addresses = if unverified {
81        read_cached_addresses(&wallet.crypto.ciphertext)?
82    } else {
83        BTreeMap::new()
84    };
85    let target_accounts = target_accounts.unwrap_or(1);
86
87    if !unverified || addresses.len() < target_accounts {
88        let prompt = "Please enter your wallet password to verify accounts: ";
89        let password = rpassword::prompt_password(prompt)?;
90        let phrase_recovered = eth_keystore::decrypt_key(&ctx.wallet_path, password)?;
91        let phrase = String::from_utf8(phrase_recovered)?;
92
93        let range = 0..max(target_accounts, DEFAULT_CACHE_ACCOUNTS);
94        derive_and_cache_addresses(ctx, &phrase, range).await
95    } else {
96        Ok(addresses)
97    }
98}
99
100/// Print collected account balances for each asset type.
101pub fn print_account_balances(
102    accounts_map: &AccountsMap,
103    account_balances: &AccountBalances,
104) -> Result<()> {
105    let mut list = List::default();
106    list.add_newline();
107    for (ix, balance) in accounts_map.keys().zip(account_balances) {
108        let balance: BTreeMap<_, _> = balance.iter().map(|(id, &val)| (id.clone(), val)).collect();
109        if balance.is_empty() {
110            continue;
111        }
112
113        list.add_separator();
114        list.add(
115            format!("Account {ix}"),
116            checksum_encode(&format!("0x{}", accounts_map[ix]))?,
117        );
118        list.add_newline();
119
120        for (asset_id, amount) in balance {
121            list.add("Asset ID", asset_id);
122            list.add("Amount", amount.to_string());
123        }
124        list.add_separator();
125    }
126    println!("{}", list);
127    Ok(())
128}
129
130pub(crate) async fn list_account_balances(
131    node_url: &Url,
132    addresses: &BTreeMap<usize, Address>,
133) -> Result<(Vec<HashMap<String, u128>>, BTreeMap<String, u128>)> {
134    println!("Connecting to {node_url}");
135    let provider = Provider::connect(&node_url).await?;
136    println!("Fetching and summing balances of the following accounts:");
137    for (ix, addr) in addresses {
138        let addr = format!("0x{}", addr);
139        let checksum_addr = checksum_encode(&addr)?;
140        println!("  {ix:>3}: {checksum_addr}");
141    }
142    let accounts: Vec<_> = addresses
143        .values()
144        .map(|addr| Wallet::new_locked(*addr, provider.clone()))
145        .collect();
146    let account_balances =
147        futures::future::try_join_all(accounts.iter().map(|acc| acc.get_balances())).await?;
148
149    let mut total_balance = BTreeMap::default();
150    for acc_bal in &account_balances {
151        for (asset_id, amt) in acc_bal {
152            let entry = total_balance.entry(asset_id.clone()).or_insert(0u128);
153            *entry = entry.checked_add(*amt).ok_or_else(|| {
154                anyhow!("Failed to display balance for asset {asset_id}: Value out of range.")
155            })?;
156        }
157    }
158
159    Ok((account_balances, total_balance))
160}
161
162pub async fn cli(ctx: &crate::CliContext, balance: &Balance) -> Result<()> {
163    let verification = if !balance.account.unverified.unverified {
164        let prompt = "Please enter your wallet password to verify accounts: ";
165        let password = rpassword::prompt_password(prompt)?;
166        AccountVerification::Yes(password)
167    } else {
168        AccountVerification::No
169    };
170
171    let addresses =
172        collect_accounts_with_verification(&ctx.wallet_path, verification, &ctx.node_url).await?;
173    let (account_balances, total_balance) =
174        list_account_balances(&ctx.node_url, &addresses).await?;
175
176    if balance.accounts {
177        print_account_balances(&addresses, &account_balances)?;
178    }
179
180    println!("\nTotal:");
181    if total_balance.is_empty() {
182        print_balance_empty(&ctx.node_url);
183    } else {
184        print_balance(&total_balance);
185    }
186    Ok(())
187}