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, bech32::Bech32Address, 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 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
76pub 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
106pub 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}