1use std::fmt::{Display, Formatter};
2
3use alloy::primitives::{map::HashMap, utils::format_units, Address, U256};
4
5use crate::{
6 alchemy::Alchemy,
7 disk::{Config, DiskInterface},
8 network::NetworkStore,
9};
10
11#[derive(Default)]
12pub struct AssetManager {
13 assets: HashMap<Address, Option<Vec<Asset>>>,
14 }
16
17impl AssetManager {
18 pub fn clear_data_for(&mut self, account: Address) {
19 self.assets.remove(&account);
20 }
21
22 pub fn update_assets(
24 &mut self,
25 account: Address,
26 mut new_assets: Vec<Asset>,
27 ) -> crate::Result<()> {
28 let old_assets = self.assets.remove(&account).flatten().unwrap_or_default();
29
30 for old_asset in old_assets {
31 if let Some(new_asset) = new_assets.iter_mut().find(|new_asset| {
32 new_asset.r#type.token_address == old_asset.r#type.token_address
33 && new_asset.r#type.network == old_asset.r#type.network
34 }) {
35 if new_asset.value == old_asset.value {
37 new_asset.light_client_verification = old_asset.light_client_verification;
38 }
39 }
40 }
41
42 self.assets.insert(account, Some(new_assets));
43
44 Ok(())
45 }
46
47 pub fn update_light_client_verification(
49 &mut self,
50 account: Address,
51 network: String,
52 token_address: TokenAddress,
53 status: LightClientVerification,
54 ) {
55 let mut assets = self.assets.remove(&account).flatten();
56
57 if let Some(assets) = assets.as_mut() {
58 for asset in assets {
59 if asset.r#type.network == network && asset.r#type.token_address == token_address {
60 asset.light_client_verification = status.clone();
61 }
62 }
63 }
64
65 self.assets.insert(account, assets);
66 }
67
68 pub fn get_assets(&self, address: &Address) -> Option<&Vec<Asset>> {
73 self.assets.get(address).and_then(|r| r.as_ref())
74 }
75}
76
77#[derive(Clone, Debug, Default, PartialEq)]
78pub enum Price {
79 #[default]
80 Pending,
81 Unknown,
82 InETH(f64),
83 InUSD(f64),
84}
85
86impl Price {
87 pub fn usd_price(&self) -> Option<f64> {
88 match self {
89 Price::InUSD(usd_price) => Some(*usd_price),
90 _ => None,
91 }
92 }
93}
94
95#[derive(Clone, Debug, PartialEq)]
96pub enum TokenAddress {
97 Native,
98 Contract(Address),
99}
100
101impl TokenAddress {
102 pub fn is_native(&self) -> bool {
103 matches!(self, TokenAddress::Native)
104 }
105
106 pub fn is_contract(&self) -> bool {
107 matches!(self, TokenAddress::Contract(_))
108 }
109}
110
111#[derive(Clone, Debug, PartialEq)]
112pub struct AssetType {
113 pub token_address: TokenAddress,
114 pub network: String,
115 pub symbol: String,
116 pub name: String,
117 pub decimals: u8,
118 pub price: Price,
119}
120
121impl Display for AssetType {
122 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
123 write!(f, "{} {}", self.symbol, self.network)
124 }
125}
126
127#[derive(Clone, Debug, PartialEq)]
128pub enum LightClientVerification {
129 Pending,
130 Verified,
131 Rejected,
132}
133
134#[derive(Clone, Debug, PartialEq)]
135pub struct Asset {
136 pub wallet_address: Address,
137 pub r#type: AssetType,
138 pub value: U256,
139 pub light_client_verification: LightClientVerification,
140}
141
142impl Asset {
143 pub fn formatted_value(&self) -> f64 {
144 let temp_formatted =
145 format_units(self.value, self.r#type.decimals).expect("format_units failed");
146
147 temp_formatted
148 .parse::<f64>()
149 .expect("parse into f64 failed")
150 }
151
152 pub fn usd_value(&self) -> Option<f64> {
153 self.r#type
154 .price
155 .usd_price()
156 .map(|usd_price| self.formatted_value() * usd_price)
157 }
158}
159
160impl Display for Asset {
161 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
162 let formatted_value = self.formatted_value();
163 let usd_value = self.usd_value();
164
165 let usd_value_fmt = usd_value.map(|v| format!(" (${v:.2})")).unwrap_or_default();
166 let light_client_status_fmt = match self.light_client_verification {
167 LightClientVerification::Pending => "",
168 LightClientVerification::Verified => " [Lightclient Verified]",
169 LightClientVerification::Rejected => " [Lightclient REJECTED]",
170 };
171
172 write!(
173 f,
174 "{formatted_value} {symbol} {network}{usd_value_fmt}{light_client_status_fmt}",
175 symbol = self.r#type.symbol,
176 network = self.r#type.network
177 )
178 }
179}
180
181#[allow(dead_code)]
182fn has_token(networks: &NetworkStore, token_address: &TokenAddress) -> bool {
183 match token_address {
184 TokenAddress::Native => false,
185 TokenAddress::Contract(address) => networks.has_token(address),
186 }
187}
188
189pub async fn get_all_assets() -> crate::Result<(Address, Vec<Asset>)> {
190 let config = Config::load()?;
191 let wallet_address = config.try_current_account()?;
192
193 let mut networks = NetworkStore::load()?;
194
195 let mut balances = Vec::new();
196
197 for entry in Alchemy::get_tokens_by_wallet(
198 wallet_address,
199 networks.get_alchemy_network_names(config.testnet_mode),
200 )
201 .await?
202 {
203 let network = networks
204 .get_by_name(&entry.network)
205 .ok_or(crate::Error::NetworkNotFound(entry.network))?;
206 let asset = Asset {
207 wallet_address,
208 r#type: AssetType {
209 token_address: match entry.token_address {
210 Some(token_address) => TokenAddress::Contract(token_address),
211 None => TokenAddress::Native,
212 },
213 network: network.name.clone(),
214 symbol: entry
215 .token_metadata
216 .symbol
217 .unwrap_or(if entry.token_address.is_none() {
218 network.symbol.unwrap_or(format!("{}ETH", network.name))
219 } else {
220 "UNKNOWN".to_string()
221 }),
222 name: entry
223 .token_metadata
224 .name
225 .unwrap_or(if entry.token_address.is_none() {
226 network.name
227 } else {
228 "UNKNOWN".to_string()
229 }),
230 decimals: entry.token_metadata.decimals.unwrap_or(
231 if entry.token_address.is_none() {
232 network.native_decimals.unwrap_or(0)
233 } else {
234 0
235 },
236 ),
237 price: entry
238 .token_prices
239 .first()
240 .map(|p| {
241 assert_eq!(p.currency, "usd"); Price::InUSD(p.value.parse().unwrap())
243 })
244 .unwrap_or(Price::Unknown),
245 },
246 value: entry.token_balance,
247 light_client_verification: LightClientVerification::Pending,
248 };
249
250 if asset.value > U256::ZERO
251 && (config.testnet_mode || asset.usd_value().map(|v| v > 0.0).unwrap_or_default())
252 {
254 balances.push(asset);
255 }
256 }
257
258 for balance in &balances {
259 if let TokenAddress::Contract(token_address) = balance.r#type.token_address {
260 networks.register_token(
261 &balance.r#type.network,
262 token_address,
263 Some(balance.r#type.symbol.as_str()),
264 &balance.r#type.name,
265 balance.r#type.decimals,
266 );
267 }
268 }
269 networks.save()?;
270
271 balances.sort_by(|a, b| {
304 a.usd_value()
305 .partial_cmp(&b.usd_value())
306 .unwrap_or(std::cmp::Ordering::Equal)
307 });
308
309 Ok((wallet_address, balances))
310}