Skip to main content

tycho_common/models/
token.rs

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
14/// Cost related to a token transfer, for example amount of gas in evm chains.
15pub type TransferCost = u64;
16
17/// Tax related to a token transfer. Should be given in Basis Points (1/100th of a percent)
18pub 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    /// Quality is between 0-100, where:
29    ///  - 100: Normal token
30    ///  - 75: Rebase token
31    ///  - 50: Fee token
32    ///  - 10: Token analysis failed at creation
33    ///  - 9-5: Token analysis failed on cronjob (after creation).
34    ///  - 0: Failed to extract decimals onchain
35    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    /// One
60    /// Get one token in BigUint format
61    ///
62    /// ## Return
63    /// Returns one token as BigUint
64    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/// Represents the quality of a token.
122///
123/// * `Good`: Indicates that the token has successfully passed the analysis process.
124/// * `Bad`: Indicates that the token has failed the analysis process. In this case, a detailed
125///   reason for the failure is provided.
126///
127/// Note: Transfer taxes do not impact the token's quality.
128/// Even if a token has transfer taxes, as long as it successfully passes the analysis,
129/// it will still be marked as `Good`.
130#[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/// A store for tracking token owners and their balances.
147///
148/// The `TokenOwnerStore` maintains a mapping between token addresses and their respective
149/// owner's address and balance. It can be used to quickly retrieve token owner information
150/// without needing to query external sources.
151#[derive(Debug)]
152pub struct TokenOwnerStore {
153    /// A `HashMap` where the key is the token address and the value is a tuple containing
154    /// the owner address and the balance of the owner for the token.
155    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        // Owner holds 1000 (unsigned big-endian).
262        let store = TokenOwnerStore::new(HashMap::from([(
263            token.clone(),
264            (owner.clone(), Bytes::from(1_000u64)),
265        )]));
266
267        // Below the requested minimum -> no adequate owner.
268        assert_eq!(
269            store
270                .find_owner(token.clone(), Bytes::from(2_000u64))
271                .await
272                .unwrap(),
273            None
274        );
275        // Exactly at the minimum -> returned.
276        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        // Above the minimum -> returned.
284        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        // Unknown token -> None.
292        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}