Skip to main content

gm_lib/utils/
assets.rs

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    // prices: HashMap<String, Price>,
15}
16
17impl AssetManager {
18    pub fn clear_data_for(&mut self, account: Address) {
19        self.assets.remove(&account);
20    }
21
22    // Update asset
23    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 balance did not change, then carry fwd some properties
36                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    // Update light client info
48    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    // Update price
69    // TODO impl
70
71    // Get fn for render which should also factor in the price
72    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"); // TODO could blow up
242                        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        // || has_token(&networks, &asset.r#type.token_address)
253        {
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    // Native balances are also fetched through Alchemy above
272    // for network in networks.get_iter(config.testnet_mode) {
273    //     let provider = network.get_provider()?;
274
275    //     let balance = provider.get_balance(wallet_address).await?;
276    //     if !balance.is_zero() {
277    //         let price = if let Some(price_ticker) = &network.price_ticker {
278    //             if price_ticker == "ETH" {
279    //                 Price::InETH(1f64)
280    //             } else {
281    //                 let (price, _) = Alchemy::get_price(price_ticker).await.expect("api failed");
282    //                 Price::InUSD(price)
283    //             }
284    //         } else {
285    //             Price::Unknown
286    //         };
287
288    //         balances.push(Asset {
289    //             wallet_address,
290    //             r#type: AssetType {
291    //                 token_address: TokenAddress::Native,
292    //                 network: network.name.clone(),
293    //                 symbol: network.symbol.clone().unwrap_or("ETH".to_string()),
294    //                 name: network.name.clone(),
295    //                 decimals: 18,
296    //                 price,
297    //             },
298    //             value: balance,
299    //         });
300    //     }
301    // }
302
303    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}