1use std::{
2 collections::HashMap,
3 hash::{Hash, Hasher},
4 sync::Arc,
5};
6
7use deepsize::DeepSizeOf;
8use num_bigint::BigUint;
9use serde::{Deserialize, Serialize};
10
11use super::{Address, Balance};
12use crate::{dto::ResponseToken, models::Chain, traits::TokenOwnerFinding, Bytes};
13
14pub type TransferCost = u64;
16
17pub type TransferTax = u64;
19
20#[derive(Debug, Clone, Deserialize, Serialize, Eq, DeepSizeOf)]
21pub struct Token {
22 pub address: Bytes,
23 pub symbol: String,
24 pub decimals: u32,
25 pub tax: TransferTax,
26 pub gas: Vec<Option<TransferCost>>,
27 pub chain: Chain,
28 pub quality: u32,
36}
37
38impl Token {
39 pub fn new(
40 address: &Bytes,
41 symbol: &str,
42 decimals: u32,
43 tax: u64,
44 gas: &[Option<u64>],
45 chain: Chain,
46 quality: u32,
47 ) -> Self {
48 Self {
49 address: address.clone(),
50 symbol: symbol.to_string(),
51 decimals,
52 tax,
53 gas: gas.to_owned(),
54 chain,
55 quality,
56 }
57 }
58
59 pub fn one(&self) -> BigUint {
65 BigUint::from((1.0 * 10f64.powi(self.decimals as i32)) as u128)
66 }
67
68 pub fn gas_usage(&self) -> BigUint {
69 BigUint::from(
70 self.gas
71 .clone()
72 .into_iter()
73 .flatten()
74 .collect::<Vec<u64>>()
75 .iter()
76 .min()
77 .copied()
78 .unwrap_or(0u64),
79 )
80 }
81}
82
83impl PartialOrd for Token {
84 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
85 self.address.partial_cmp(&other.address)
86 }
87}
88
89impl PartialEq for Token {
90 fn eq(&self, other: &Self) -> bool {
91 self.address == other.address
92 }
93}
94
95impl Hash for Token {
96 fn hash<H: Hasher>(&self, state: &mut H) {
97 self.address.hash(state);
98 }
99}
100
101impl From<Arc<Token>> for Address {
102 fn from(val: Arc<Token>) -> Self {
103 val.address.clone()
104 }
105}
106
107impl From<ResponseToken> for Token {
108 fn from(value: ResponseToken) -> Self {
109 Self {
110 chain: value.chain.into(),
111 address: value.address,
112 symbol: value.symbol,
113 decimals: value.decimals,
114 tax: value.tax,
115 gas: value.gas,
116 quality: value.quality,
117 }
118 }
119}
120
121#[derive(Debug, Clone, Eq, PartialEq)]
131pub enum TokenQuality {
132 Good,
133 Bad { reason: String },
134}
135
136impl TokenQuality {
137 pub fn is_good(&self) -> bool {
138 matches!(self, Self::Good { .. })
139 }
140
141 pub fn bad(reason: impl ToString) -> Self {
142 Self::Bad { reason: reason.to_string() }
143 }
144}
145
146#[derive(Debug)]
152pub struct TokenOwnerStore {
153 values: HashMap<Address, (Address, Balance)>,
156}
157
158impl TokenOwnerStore {
159 pub fn new(values: HashMap<Address, (Address, Balance)>) -> Self {
160 TokenOwnerStore { values }
161 }
162}
163
164#[async_trait::async_trait]
165impl TokenOwnerFinding for TokenOwnerStore {
166 async fn find_owner(
167 &self,
168 token: Address,
169 min_balance: Balance,
170 ) -> Result<Option<(Address, Balance)>, String> {
171 Ok(self
172 .values
173 .get(&token)
174 .filter(|(_, balance)| {
175 BigUint::from_bytes_be(balance.as_ref()) >=
176 BigUint::from_bytes_be(min_balance.as_ref())
177 })
178 .cloned())
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use std::str::FromStr;
185
186 use super::*;
187
188 #[test]
189 fn test_constructor() {
190 let token = Token::new(
191 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
192 "USDC",
193 6,
194 1000,
195 &[Some(1000u64)],
196 Chain::Ethereum,
197 100,
198 );
199
200 assert_eq!(token.symbol, "USDC");
201 assert_eq!(token.decimals, 6);
202 assert_eq!(
203 format!("{token_address:#x}", token_address = token.address),
204 "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
205 );
206 }
207
208 #[test]
209 fn test_cmp() {
210 let usdc = Token::new(
211 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
212 "USDC",
213 6,
214 1000,
215 &[Some(1000u64)],
216 Chain::Ethereum,
217 100,
218 );
219 let usdc2 = Token::new(
220 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
221 "USDC2",
222 6,
223 1000,
224 &[Some(1000u64)],
225 Chain::Ethereum,
226 100,
227 );
228 let weth = Token::new(
229 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
230 "WETH",
231 18,
232 1000,
233 &[Some(1000u64)],
234 Chain::Ethereum,
235 100,
236 );
237
238 assert!(usdc < weth);
239 assert_eq!(usdc, usdc2);
240 }
241
242 #[test]
243 fn test_one() {
244 let usdc = Token::new(
245 &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
246 "USDC",
247 6,
248 1000,
249 &[Some(1000u64)],
250 Chain::Ethereum,
251 100,
252 );
253
254 assert_eq!(usdc.one(), BigUint::from(1000000u64));
255 }
256
257 #[tokio::test]
258 async fn test_find_owner_respects_min_balance() {
259 let token = Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
260 let owner = Bytes::from_str("0x1111111111111111111111111111111111111111").unwrap();
261 let store = TokenOwnerStore::new(HashMap::from([(
263 token.clone(),
264 (owner.clone(), Bytes::from(1_000u64)),
265 )]));
266
267 assert_eq!(
269 store
270 .find_owner(token.clone(), Bytes::from(2_000u64))
271 .await
272 .unwrap(),
273 None
274 );
275 assert_eq!(
277 store
278 .find_owner(token.clone(), Bytes::from(1_000u64))
279 .await
280 .unwrap(),
281 Some((owner.clone(), Bytes::from(1_000u64)))
282 );
283 assert_eq!(
285 store
286 .find_owner(token.clone(), Bytes::from(500u64))
287 .await
288 .unwrap(),
289 Some((owner, Bytes::from(1_000u64)))
290 );
291 let other = Bytes::from_str("0x2222222222222222222222222222222222222222").unwrap();
293 assert_eq!(
294 store
295 .find_owner(other, Bytes::from(1u64))
296 .await
297 .unwrap(),
298 None
299 );
300 }
301}