tycho_common/models/
token.rs

1use std::{
2    collections::HashMap,
3    hash::{Hash, Hasher},
4};
5
6use num_bigint::BigUint;
7use serde::{Deserialize, Serialize};
8
9use super::{Address, Balance};
10use crate::{dto::ResponseToken, models::Chain, traits::TokenOwnerFinding, Bytes};
11
12/// Cost related to a token transfer, for example amount of gas in evm chains.
13pub type TransferCost = u64;
14
15/// Tax related to a token transfer. Should be given in Basis Points (1/100th of a percent)
16pub type TransferTax = u64;
17
18#[derive(Debug, Clone, Deserialize, Serialize, Eq)]
19pub struct Token {
20    pub address: Bytes,
21    pub symbol: String,
22    pub decimals: u32,
23    pub tax: TransferTax,
24    pub gas: Vec<Option<TransferCost>>,
25    pub chain: Chain,
26    /// Quality is between 0-100, where:
27    ///  - 100: Normal token
28    ///  - 75: Rebase token
29    ///  - 50: Fee token
30    ///  - 10: Token analysis failed at creation
31    ///  - 9-5: Token analysis failed on cronjob (after creation).
32    ///  - 0: Failed to extract decimals onchain
33    pub quality: u32,
34}
35
36impl Token {
37    pub fn new(
38        address: &Bytes,
39        symbol: &str,
40        decimals: u32,
41        tax: u64,
42        gas: &[Option<u64>],
43        chain: Chain,
44        quality: u32,
45    ) -> Self {
46        Self {
47            address: address.clone(),
48            symbol: symbol.to_string(),
49            decimals,
50            tax,
51            gas: gas.to_owned(),
52            chain,
53            quality,
54        }
55    }
56
57    /// One
58    /// Get one token in BigUint format
59    ///
60    /// ## Return
61    /// Returns one token as BigUint
62    pub fn one(&self) -> BigUint {
63        BigUint::from((1.0 * 10f64.powi(self.decimals as i32)) as u128)
64    }
65}
66
67impl PartialOrd for Token {
68    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
69        self.address.partial_cmp(&other.address)
70    }
71}
72
73impl PartialEq for Token {
74    fn eq(&self, other: &Self) -> bool {
75        self.address == other.address
76    }
77}
78
79impl Hash for Token {
80    fn hash<H: Hasher>(&self, state: &mut H) {
81        self.address.hash(state);
82    }
83}
84
85impl TryFrom<ResponseToken> for Token {
86    type Error = ();
87
88    fn try_from(value: ResponseToken) -> Result<Self, Self::Error> {
89        Ok(Self {
90            address: value.address,
91            decimals: value.decimals,
92            symbol: value.symbol.to_string(),
93            gas: value.gas,
94            chain: Chain::from(value.chain),
95            tax: value.tax,
96            quality: value.quality,
97        })
98    }
99}
100
101/// Represents the quality of a token.
102///
103/// * `Good`: Indicates that the token has successfully passed the analysis process.
104/// * `Bad`: Indicates that the token has failed the analysis process. In this case, a detailed
105///   reason for the failure is provided.
106///
107/// Note: Transfer taxes do not impact the token's quality.
108/// Even if a token has transfer taxes, as long as it successfully passes the analysis,
109/// it will still be marked as `Good`.
110#[derive(Debug, Clone, Eq, PartialEq)]
111pub enum TokenQuality {
112    Good,
113    Bad { reason: String },
114}
115
116impl TokenQuality {
117    pub fn is_good(&self) -> bool {
118        matches!(self, Self::Good { .. })
119    }
120
121    pub fn bad(reason: impl ToString) -> Self {
122        Self::Bad { reason: reason.to_string() }
123    }
124}
125
126/// A store for tracking token owners and their balances.
127///
128/// The `TokenOwnerStore` maintains a mapping between token addresses and their respective
129/// owner's address and balance. It can be used to quickly retrieve token owner information
130/// without needing to query external sources.
131///
132/// # Fields
133/// * `values` - A `HashMap` where:
134///   * The key is the token `Address`, representing the address of the token being tracked.
135///   * The value is a tuple containing:
136///     * The owner `Address` of the token.
137///     * The `Balance` of the owner for the token.
138#[derive(Debug)]
139pub struct TokenOwnerStore {
140    values: HashMap<Address, (Address, Balance)>,
141}
142
143impl TokenOwnerStore {
144    pub fn new(values: HashMap<Address, (Address, Balance)>) -> Self {
145        TokenOwnerStore { values }
146    }
147}
148
149#[async_trait::async_trait]
150impl TokenOwnerFinding for TokenOwnerStore {
151    async fn find_owner(
152        &self,
153        token: Address,
154        _min_balance: Balance,
155    ) -> Result<Option<(Address, Balance)>, String> {
156        Ok(self.values.get(&token).cloned())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use std::str::FromStr;
163
164    use super::*;
165
166    #[test]
167    fn test_constructor() {
168        let token = Token::new(
169            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
170            "USDC",
171            6,
172            1000,
173            &[Some(1000u64)],
174            Chain::Ethereum,
175            100,
176        );
177
178        assert_eq!(token.symbol, "USDC");
179        assert_eq!(token.decimals, 6);
180        assert_eq!(
181            format!("{token_address:#x}", token_address = token.address),
182            "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
183        );
184    }
185
186    #[test]
187    fn test_cmp() {
188        let usdc = Token::new(
189            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
190            "USDC",
191            6,
192            1000,
193            &[Some(1000u64)],
194            Chain::Ethereum,
195            100,
196        );
197        let usdc2 = Token::new(
198            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
199            "USDC2",
200            6,
201            1000,
202            &[Some(1000u64)],
203            Chain::Ethereum,
204            100,
205        );
206        let weth = Token::new(
207            &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
208            "WETH",
209            18,
210            1000,
211            &[Some(1000u64)],
212            Chain::Ethereum,
213            100,
214        );
215
216        assert!(usdc < weth);
217        assert_eq!(usdc, usdc2);
218    }
219
220    #[test]
221    fn test_one() {
222        let usdc = Token::new(
223            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
224            "USDC",
225            6,
226            1000,
227            &[Some(1000u64)],
228            Chain::Ethereum,
229            100,
230        );
231
232        assert_eq!(usdc.one(), BigUint::from(1000000u64));
233    }
234}