use std::fmt::{Display, Formatter};
use alloy::primitives::{map::HashMap, utils::format_units, Address, U256};
use crate::{
alchemy::Alchemy,
disk::{Config, DiskInterface},
network::NetworkStore,
};
#[derive(Default)]
pub struct AssetManager {
assets: HashMap<Address, Option<Vec<Asset>>>,
}
impl AssetManager {
pub fn clear_data_for(&mut self, account: Address) {
self.assets.remove(&account);
}
pub fn update_assets(
&mut self,
account: Address,
mut new_assets: Vec<Asset>,
) -> crate::Result<()> {
let old_assets = self.assets.remove(&account).flatten().unwrap_or_default();
for old_asset in old_assets {
if let Some(new_asset) = new_assets.iter_mut().find(|new_asset| {
new_asset.r#type.token_address == old_asset.r#type.token_address
&& new_asset.r#type.network == old_asset.r#type.network
}) {
if new_asset.value == old_asset.value {
new_asset.light_client_verification = old_asset.light_client_verification;
}
}
}
self.assets.insert(account, Some(new_assets));
Ok(())
}
pub fn update_light_client_verification(
&mut self,
account: Address,
network: String,
token_address: TokenAddress,
status: LightClientVerification,
) {
let mut assets = self.assets.remove(&account).flatten();
if let Some(assets) = assets.as_mut() {
for asset in assets {
if asset.r#type.network == network && asset.r#type.token_address == token_address {
asset.light_client_verification = status.clone();
}
}
}
self.assets.insert(account, assets);
}
pub fn get_assets(&self, address: &Address) -> Option<&Vec<Asset>> {
self.assets.get(address).and_then(|r| r.as_ref())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum Price {
#[default]
Pending,
Unknown,
InETH(f64),
InUSD(f64),
}
impl Price {
pub fn usd_price(&self) -> Option<f64> {
match self {
Price::InUSD(usd_price) => Some(*usd_price),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TokenAddress {
Native,
Contract(Address),
}
impl TokenAddress {
pub fn is_native(&self) -> bool {
matches!(self, TokenAddress::Native)
}
pub fn is_contract(&self) -> bool {
matches!(self, TokenAddress::Contract(_))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AssetType {
pub token_address: TokenAddress,
pub network: String,
pub symbol: String,
pub name: String,
pub decimals: u8,
pub price: Price,
}
impl Display for AssetType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.symbol, self.network)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum LightClientVerification {
Pending,
Verified,
Rejected,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Asset {
pub wallet_address: Address,
pub r#type: AssetType,
pub value: U256,
pub light_client_verification: LightClientVerification,
}
impl Asset {
pub fn formatted_value(&self) -> f64 {
let temp_formatted =
format_units(self.value, self.r#type.decimals).expect("format_units failed");
temp_formatted
.parse::<f64>()
.expect("parse into f64 failed")
}
pub fn usd_value(&self) -> Option<f64> {
self.r#type
.price
.usd_price()
.map(|usd_price| self.formatted_value() * usd_price)
}
}
impl Display for Asset {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let formatted_value = self.formatted_value();
let usd_value = self.usd_value();
let usd_value_fmt = usd_value.map(|v| format!(" (${v:.2})")).unwrap_or_default();
let light_client_status_fmt = match self.light_client_verification {
LightClientVerification::Pending => "",
LightClientVerification::Verified => " [Lightclient Verified]",
LightClientVerification::Rejected => " [Lightclient REJECTED]",
};
write!(
f,
"{formatted_value} {symbol} {network}{usd_value_fmt}{light_client_status_fmt}",
symbol = self.r#type.symbol,
network = self.r#type.network
)
}
}
#[allow(dead_code)]
fn has_token(networks: &NetworkStore, token_address: &TokenAddress) -> bool {
match token_address {
TokenAddress::Native => false,
TokenAddress::Contract(address) => networks.has_token(address),
}
}
pub async fn get_all_assets() -> crate::Result<(Address, Vec<Asset>)> {
let config = Config::load()?;
let wallet_address = config.try_current_account()?;
let mut networks = NetworkStore::load()?;
let mut balances = Vec::new();
for entry in Alchemy::get_tokens_by_wallet(
wallet_address,
networks.get_alchemy_network_names(config.testnet_mode),
)
.await?
{
let network = networks
.get_by_name(&entry.network)
.ok_or(crate::Error::NetworkNotFound(entry.network))?;
let asset = Asset {
wallet_address,
r#type: AssetType {
token_address: match entry.token_address {
Some(token_address) => TokenAddress::Contract(token_address),
None => TokenAddress::Native,
},
network: network.name.clone(),
symbol: entry
.token_metadata
.symbol
.unwrap_or(if entry.token_address.is_none() {
network.symbol.unwrap_or(format!("{}ETH", network.name))
} else {
"UNKNOWN".to_string()
}),
name: entry
.token_metadata
.name
.unwrap_or(if entry.token_address.is_none() {
network.name
} else {
"UNKNOWN".to_string()
}),
decimals: entry.token_metadata.decimals.unwrap_or(
if entry.token_address.is_none() {
network.native_decimals.unwrap_or(0)
} else {
0
},
),
price: entry
.token_prices
.first()
.map(|p| {
assert_eq!(p.currency, "usd"); Price::InUSD(p.value.parse().unwrap())
})
.unwrap_or(Price::Unknown),
},
value: entry.token_balance,
light_client_verification: LightClientVerification::Pending,
};
if asset.value > U256::ZERO
&& (config.testnet_mode || asset.usd_value().map(|v| v > 0.0).unwrap_or_default())
{
balances.push(asset);
}
}
for balance in &balances {
if let TokenAddress::Contract(token_address) = balance.r#type.token_address {
networks.register_token(
&balance.r#type.network,
token_address,
Some(balance.r#type.symbol.as_str()),
&balance.r#type.name,
balance.r#type.decimals,
);
}
}
networks.save()?;
balances.sort_by(|a, b| {
a.usd_value()
.partial_cmp(&b.usd_value())
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok((wallet_address, balances))
}