use std::{collections::HashSet, fmt::Display};
use alloy_primitives::{Address, U256};
use crate::{
defi::Token,
types::{Money, Quantity},
};
#[derive(Debug)]
pub struct TokenBalance {
pub amount: U256,
pub amount_usd: Option<Quantity>,
pub token: Token,
}
impl TokenBalance {
pub const fn new(amount: U256, token: Token) -> Self {
Self {
amount,
token,
amount_usd: None,
}
}
pub fn as_quantity(&self) -> anyhow::Result<Quantity> {
Quantity::from_u256(self.amount, self.token.decimals)
}
pub fn set_amount_usd(&mut self, amount_usd: Quantity) {
self.amount_usd = Some(amount_usd);
}
}
impl Display for TokenBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let quantity = self.as_quantity().unwrap_or_default();
match &self.amount_usd {
Some(usd) => write!(
f,
"TokenBalance(token={}, amount={}, usd=${:.2})",
self.token.symbol,
quantity.as_decimal(),
usd.as_f64()
),
None => write!(
f,
"TokenBalance(token={}, amount={})",
self.token.symbol,
quantity.as_decimal()
),
}
}
}
#[derive(Debug)]
pub struct WalletBalance {
pub native_currency: Option<Money>,
pub token_balances: Vec<TokenBalance>,
pub token_universe: HashSet<Address>,
}
impl WalletBalance {
pub const fn new(token_universe: HashSet<Address>) -> Self {
Self {
native_currency: None,
token_balances: vec![],
token_universe,
}
}
pub fn is_token_universe_initialized(&self) -> bool {
!self.token_universe.is_empty()
}
pub fn set_native_currency_balance(&mut self, balance: Money) {
self.native_currency = Some(balance);
}
pub fn add_token_balance(&mut self, token_balance: TokenBalance) {
self.token_balances.push(token_balance);
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use alloy_primitives::{U256, address};
use rstest::rstest;
use super::*;
use crate::defi::{
SharedChain, Token,
chain::chains,
stubs::{arbitrum, usdc, weth},
};
fn create_token(symbol: &str, decimals: u8) -> Token {
Token::new(
Arc::new(chains::ETHEREUM.clone()),
address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
format!("{symbol} Token"),
symbol.to_string(),
decimals,
)
}
#[rstest]
fn test_token_balance_as_quantity_18_decimals(#[from(arbitrum)] chain: SharedChain) {
let token = Token::new(
chain,
address!("0x4fE83213D56308330EC302a8BD641f1d0113A4Cc"),
"NuCypher".to_string(),
"NU".to_string(),
18,
);
let amount = U256::from(10342u64) * U256::from(10u64).pow(U256::from(18u64));
let balance = TokenBalance::new(amount, token);
let quantity = balance.as_quantity().unwrap();
assert_eq!(
quantity.as_decimal().to_string(),
"10342.000000000000000000"
);
}
#[rstest]
fn test_token_balance_as_quantity_6_decimals() {
let token = create_token("USDC", 6);
let amount = U256::from(92220728254u64);
let balance = TokenBalance::new(amount, token);
let quantity = balance.as_quantity().unwrap();
assert_eq!(quantity.as_decimal().to_string(), "92220.728254");
}
#[rstest]
fn test_token_balance_as_quantity_fractional_18_decimals(#[from(arbitrum)] chain: SharedChain) {
let token = Token::new(
chain,
address!("0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa"),
"mETH".to_string(),
"mETH".to_string(),
18,
);
let amount = U256::from(758325512078001391u64);
let balance = TokenBalance::new(amount, token);
let quantity = balance.as_quantity().unwrap();
assert_eq!(quantity.as_decimal().to_string(), "0.758325512078001391");
}
#[rstest]
fn test_token_balance_display_18_decimals(#[from(arbitrum)] chain: SharedChain) {
let token = Token::new(
chain,
address!("0x912CE59144191C1204E64559FE8253a0e49E6548"),
"Arbitrum".to_string(),
"ARB".to_string(),
18,
);
let amount = U256::from_str_radix("7922013795343949480329", 10).unwrap();
let balance = TokenBalance::new(amount, token);
let display = balance.to_string();
assert!(display.contains("ARB"));
assert!(display.contains("7922.013795343949480329"));
}
#[rstest]
fn test_token_balance_display_6_decimals() {
let token = create_token("USDC", 6);
let amount = U256::from(92220728254u64); let balance = TokenBalance::new(amount, token);
let display = balance.to_string();
assert!(display.contains("USDC"));
assert!(display.contains("92220.728254"));
}
#[rstest]
fn test_token_balance_set_amount_usd(weth: Token) {
let amount = U256::from(1u64) * U256::from(10u64).pow(U256::from(18u64));
let mut balance = TokenBalance::new(amount, weth);
assert!(balance.amount_usd.is_none());
let usd_value = Quantity::from("3500.00");
balance.set_amount_usd(usd_value);
assert!(balance.amount_usd.is_some());
assert_eq!(
balance.amount_usd.unwrap().as_decimal().to_string(),
"3500.00"
);
}
#[rstest]
fn test_wallet_balance_new_empty() {
let wallet = WalletBalance::new(HashSet::new());
assert!(wallet.native_currency.is_none());
assert!(wallet.token_balances.is_empty());
assert!(!wallet.is_token_universe_initialized());
}
#[rstest]
fn test_wallet_balance_with_token_universe() {
let mut tokens = HashSet::new();
tokens.insert(address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")); tokens.insert(address!("0x912CE59144191C1204E64559FE8253a0e49E6548"));
let wallet = WalletBalance::new(tokens);
assert!(wallet.is_token_universe_initialized());
assert_eq!(wallet.token_universe.len(), 2);
}
#[rstest]
fn test_wallet_balance_set_native_currency() {
let mut wallet = WalletBalance::new(HashSet::new());
assert!(wallet.native_currency.is_none());
let eth_balance = Money::new(50.936054, crate::types::Currency::ETH());
wallet.set_native_currency_balance(eth_balance);
assert!(wallet.native_currency.is_some());
}
#[rstest]
fn test_wallet_balance_add_token_balance(usdc: Token, weth: Token) {
let mut wallet = WalletBalance::new(HashSet::new());
let usdc_balance = TokenBalance::new(U256::from(100_000_000u64), usdc); let weth_balance = TokenBalance::new(U256::from(10u64).pow(U256::from(18u64)), weth);
wallet.add_token_balance(usdc_balance);
wallet.add_token_balance(weth_balance);
assert_eq!(wallet.token_balances.len(), 2);
assert_eq!(wallet.token_balances[0].token.symbol, "USDC");
assert_eq!(wallet.token_balances[1].token.symbol, "WETH");
}
}