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 #[clap(flatten)]
28 pub(crate) account: crate::account::Balance,
29 #[clap(long)]
32 pub(crate) accounts: bool,
33}
34
35pub enum AccountVerification {
39 No,
40 Yes(String),
41}
42
43pub type AccountBalances = Vec<HashMap<String, u128>>;
45pub type AccountsMap = BTreeMap<usize, Address>;
47
48pub 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
70pub 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
100pub 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}