nautilus_model/defi/
wallet.rs1use std::{collections::HashSet, fmt::Display};
17
18use alloy_primitives::{Address, U256};
19
20use crate::{
21 defi::Token,
22 types::{Money, Quantity},
23};
24
25#[derive(Debug)]
30pub struct TokenBalance {
31 pub amount: U256,
33 pub amount_usd: Option<Quantity>,
35 pub token: Token,
37}
38
39impl TokenBalance {
40 pub const fn new(amount: U256, token: Token) -> Self {
42 Self {
43 amount,
44 token,
45 amount_usd: None,
46 }
47 }
48
49 pub fn as_quantity(&self) -> anyhow::Result<Quantity> {
55 Quantity::from_u256(self.amount, self.token.decimals)
56 }
57
58 pub fn set_amount_usd(&mut self, amount_usd: Quantity) {
60 self.amount_usd = Some(amount_usd);
61 }
62}
63
64impl Display for TokenBalance {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 let quantity = self.as_quantity().unwrap_or_default();
67 match &self.amount_usd {
68 Some(usd) => write!(
69 f,
70 "TokenBalance(token={}, amount={}, usd=${:.2})",
71 self.token.symbol,
72 quantity.as_decimal(),
73 usd.as_f64()
74 ),
75 None => write!(
76 f,
77 "TokenBalance(token={}, amount={})",
78 self.token.symbol,
79 quantity.as_decimal()
80 ),
81 }
82 }
83}
84
85#[derive(Debug)]
91pub struct WalletBalance {
92 pub native_currency: Option<Money>,
94 pub token_balances: Vec<TokenBalance>,
96 pub token_universe: HashSet<Address>,
98}
99
100impl WalletBalance {
101 pub const fn new(token_universe: HashSet<Address>) -> Self {
103 Self {
104 native_currency: None,
105 token_balances: vec![],
106 token_universe,
107 }
108 }
109
110 pub fn is_token_universe_initialized(&self) -> bool {
112 !self.token_universe.is_empty()
113 }
114
115 pub fn set_native_currency_balance(&mut self, balance: Money) {
117 self.native_currency = Some(balance);
118 }
119
120 pub fn add_token_balance(&mut self, token_balance: TokenBalance) {
122 self.token_balances.push(token_balance);
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use std::sync::Arc;
129
130 use alloy_primitives::{U256, address};
131 use rstest::rstest;
132
133 use super::*;
134 use crate::defi::{
135 SharedChain, Token,
136 chain::chains,
137 stubs::{arbitrum, usdc, weth},
138 };
139
140 fn create_token(symbol: &str, decimals: u8) -> Token {
142 Token::new(
143 Arc::new(chains::ETHEREUM.clone()),
144 address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
145 format!("{symbol} Token"),
146 symbol.to_string(),
147 decimals,
148 )
149 }
150
151 #[rstest]
152 fn test_token_balance_as_quantity_18_decimals(#[from(arbitrum)] chain: SharedChain) {
153 let token = Token::new(
157 chain,
158 address!("0x4fE83213D56308330EC302a8BD641f1d0113A4Cc"),
159 "NuCypher".to_string(),
160 "NU".to_string(),
161 18,
162 );
163 let amount = U256::from(10342u64) * U256::from(10u64).pow(U256::from(18u64));
164 let balance = TokenBalance::new(amount, token);
165
166 let quantity = balance.as_quantity().unwrap();
167 assert_eq!(
168 quantity.as_decimal().to_string(),
169 "10342.000000000000000000"
170 );
171 }
172
173 #[rstest]
174 fn test_token_balance_as_quantity_6_decimals() {
175 let token = create_token("USDC", 6);
179 let amount = U256::from(92220728254u64);
180 let balance = TokenBalance::new(amount, token);
181
182 let quantity = balance.as_quantity().unwrap();
183 assert_eq!(quantity.as_decimal().to_string(), "92220.728254");
184 }
185
186 #[rstest]
187 fn test_token_balance_as_quantity_fractional_18_decimals(#[from(arbitrum)] chain: SharedChain) {
188 let token = Token::new(
192 chain,
193 address!("0xd5F7838F5C461fefF7FE49ea5ebaF7728bB0ADfa"),
194 "mETH".to_string(),
195 "mETH".to_string(),
196 18,
197 );
198 let amount = U256::from(758325512078001391u64);
199 let balance = TokenBalance::new(amount, token);
200
201 let quantity = balance.as_quantity().unwrap();
202 assert_eq!(quantity.as_decimal().to_string(), "0.758325512078001391");
203 }
204
205 #[rstest]
206 fn test_token_balance_display_18_decimals(#[from(arbitrum)] chain: SharedChain) {
207 let token = Token::new(
209 chain,
210 address!("0x912CE59144191C1204E64559FE8253a0e49E6548"),
211 "Arbitrum".to_string(),
212 "ARB".to_string(),
213 18,
214 );
215 let amount = U256::from_str_radix("7922013795343949480329", 10).unwrap();
217 let balance = TokenBalance::new(amount, token);
218
219 let display = balance.to_string();
220 assert!(display.contains("ARB"));
221 assert!(display.contains("7922.013795343949480329"));
222 }
223
224 #[rstest]
225 fn test_token_balance_display_6_decimals() {
226 let token = create_token("USDC", 6);
228 let amount = U256::from(92220728254u64); let balance = TokenBalance::new(amount, token);
230
231 let display = balance.to_string();
232 assert!(display.contains("USDC"));
233 assert!(display.contains("92220.728254"));
234 }
235
236 #[rstest]
237 fn test_token_balance_set_amount_usd(weth: Token) {
238 let amount = U256::from(1u64) * U256::from(10u64).pow(U256::from(18u64));
239 let mut balance = TokenBalance::new(amount, weth);
240
241 assert!(balance.amount_usd.is_none());
242
243 let usd_value = Quantity::from("3500.00");
244 balance.set_amount_usd(usd_value);
245
246 assert!(balance.amount_usd.is_some());
247 assert_eq!(
248 balance.amount_usd.unwrap().as_decimal().to_string(),
249 "3500.00"
250 );
251 }
252
253 #[rstest]
254 fn test_wallet_balance_new_empty() {
255 let wallet = WalletBalance::new(HashSet::new());
256
257 assert!(wallet.native_currency.is_none());
258 assert!(wallet.token_balances.is_empty());
259 assert!(!wallet.is_token_universe_initialized());
260 }
261
262 #[rstest]
263 fn test_wallet_balance_with_token_universe() {
264 let mut tokens = HashSet::new();
265 tokens.insert(address!("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")); tokens.insert(address!("0x912CE59144191C1204E64559FE8253a0e49E6548")); let wallet = WalletBalance::new(tokens);
269
270 assert!(wallet.is_token_universe_initialized());
271 assert_eq!(wallet.token_universe.len(), 2);
272 }
273
274 #[rstest]
275 fn test_wallet_balance_set_native_currency() {
276 let mut wallet = WalletBalance::new(HashSet::new());
277
278 assert!(wallet.native_currency.is_none());
279
280 let eth_balance = Money::new(50.936054, crate::types::Currency::ETH());
281 wallet.set_native_currency_balance(eth_balance);
282
283 assert!(wallet.native_currency.is_some());
284 }
285
286 #[rstest]
287 fn test_wallet_balance_add_token_balance(usdc: Token, weth: Token) {
288 let mut wallet = WalletBalance::new(HashSet::new());
289
290 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);
294 wallet.add_token_balance(weth_balance);
295
296 assert_eq!(wallet.token_balances.len(), 2);
297 assert_eq!(wallet.token_balances[0].token.symbol, "USDC");
298 assert_eq!(wallet.token_balances[1].token.symbol, "WETH");
299 }
300}