forc_wallet/
balance.rs

1use crate::{
2    account::{
3        derive_account_unlocked, derive_and_cache_addresses, print_balance, print_balance_empty,
4        read_cached_addresses, verify_address_and_update_cache,
5    },
6    format::List,
7    utils::load_wallet,
8    DEFAULT_CACHE_ACCOUNTS,
9};
10use anyhow::{anyhow, Result};
11use clap::Args;
12use fuels::{
13    accounts::{provider::Provider, wallet::Wallet, ViewOnlyAccount},
14    types::{bech32::Bech32Address, checksum_address::checksum_encode, Address},
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 bech32 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 `Bech32Address` 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 addr_bech32 = Bech32Address::from(*addr);
60            let provider = Provider::connect(node_url).await?;
61            let account = derive_account_unlocked(wallet_path, ix, &password, &provider)?;
62            if verify_address_and_update_cache(
63                ix,
64                &account,
65                &addr_bech32,
66                &wallet.crypto.ciphertext,
67            )? {
68                *addr = account.address().clone().into();
69            }
70        }
71    }
72
73    Ok(addresses)
74}
75
76/// Returns N derived addresses. If the `unverified` flag is set, it will not verify the addresses
77/// and will use the cached ones.
78///
79/// This function will override / fix the cached addresses if the user password is requested
80pub async fn get_derived_accounts(
81    ctx: &crate::CliContext,
82    unverified: bool,
83    target_accounts: Option<usize>,
84) -> Result<AccountsMap> {
85    let wallet = load_wallet(&ctx.wallet_path)?;
86    let addresses = if unverified {
87        read_cached_addresses(&wallet.crypto.ciphertext)?
88    } else {
89        BTreeMap::new()
90    };
91    let target_accounts = target_accounts.unwrap_or(1);
92
93    if !unverified || addresses.len() < target_accounts {
94        let prompt = "Please enter your wallet password to verify accounts: ";
95        let password = rpassword::prompt_password(prompt)?;
96        let phrase_recovered = eth_keystore::decrypt_key(&ctx.wallet_path, password)?;
97        let phrase = String::from_utf8(phrase_recovered)?;
98
99        let range = 0..max(target_accounts, DEFAULT_CACHE_ACCOUNTS);
100        derive_and_cache_addresses(ctx, &phrase, range).await
101    } else {
102        Ok(addresses)
103    }
104}
105
106/// Print collected account balances for each asset type.
107pub fn print_account_balances(
108    accounts_map: &AccountsMap,
109    account_balances: &AccountBalances,
110) -> Result<()> {
111    let mut list = List::default();
112    list.add_newline();
113    for (ix, balance) in accounts_map.keys().zip(account_balances) {
114        let balance: BTreeMap<_, _> = balance.iter().map(|(id, &val)| (id.clone(), val)).collect();
115        if balance.is_empty() {
116            continue;
117        }
118
119        list.add_seperator();
120        list.add(
121            format!("Account {ix}"),
122            checksum_encode(&format!("0x{}", accounts_map[ix]))?,
123        );
124        list.add_newline();
125
126        for (asset_id, amount) in balance {
127            list.add("Asset ID", asset_id);
128            list.add("Amount", amount.to_string());
129        }
130        list.add_seperator();
131    }
132    println!("{}", list);
133    Ok(())
134}
135
136pub(crate) async fn list_account_balances(
137    node_url: &Url,
138    addresses: &BTreeMap<usize, Address>,
139) -> Result<(Vec<HashMap<String, u128>>, BTreeMap<String, u128>)> {
140    println!("Connecting to {node_url}");
141    let provider = Provider::connect(&node_url).await?;
142    println!("Fetching and summing balances of the following accounts:");
143    for (ix, addr) in addresses {
144        let addr = format!("0x{}", addr);
145        let checksum_addr = checksum_encode(&addr)?;
146        println!("  {ix:>3}: {checksum_addr}");
147    }
148    let accounts: Vec<_> = addresses
149        .values()
150        .map(|addr| Wallet::new_locked(Bech32Address::from(*addr), provider.clone()))
151        .collect();
152    let account_balances =
153        futures::future::try_join_all(accounts.iter().map(|acc| acc.get_balances())).await?;
154
155    let mut total_balance = BTreeMap::default();
156    for acc_bal in &account_balances {
157        for (asset_id, amt) in acc_bal {
158            let entry = total_balance.entry(asset_id.clone()).or_insert(0u128);
159            *entry = entry.checked_add(*amt).ok_or_else(|| {
160                anyhow!("Failed to display balance for asset {asset_id}: Value out of range.")
161            })?;
162        }
163    }
164
165    Ok((account_balances, total_balance))
166}
167
168pub async fn cli(ctx: &crate::CliContext, balance: &Balance) -> Result<()> {
169    let verification = if !balance.account.unverified.unverified {
170        let prompt = "Please enter your wallet password to verify accounts: ";
171        let password = rpassword::prompt_password(prompt)?;
172        AccountVerification::Yes(password)
173    } else {
174        AccountVerification::No
175    };
176
177    let addresses =
178        collect_accounts_with_verification(&ctx.wallet_path, verification, &ctx.node_url).await?;
179    let (account_balances, total_balance) =
180        list_account_balances(&ctx.node_url, &addresses).await?;
181
182    if balance.accounts {
183        print_account_balances(&addresses, &account_balances)?;
184    }
185
186    println!("\nTotal:");
187    if total_balance.is_empty() {
188        print_balance_empty(&ctx.node_url);
189    } else {
190        print_balance(&total_balance);
191    }
192    Ok(())
193}