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    pub fn gas_usage(&self) -> BigUint {
67        BigUint::from(
68            self.gas
69                .clone()
70                .into_iter()
71                .flatten()
72                .collect::<Vec<u64>>()
73                .iter()
74                .min()
75                .copied()
76                .unwrap_or(0u64),
77        )
78    }
79}
80
81impl PartialOrd for Token {
82    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
83        self.address.partial_cmp(&other.address)
84    }
85}
86
87impl PartialEq for Token {
88    fn eq(&self, other: &Self) -> bool {
89        self.address == other.address
90    }
91}
92
93impl Hash for Token {
94    fn hash<H: Hasher>(&self, state: &mut H) {
95        self.address.hash(state);
96    }
97}
98
99impl TryFrom<ResponseToken> for Token {
100    type Error = ();
101
102    fn try_from(value: ResponseToken) -> Result<Self, Self::Error> {
103        Ok(Self {
104            address: value.address,
105            decimals: value.decimals,
106            symbol: value.symbol.to_string(),
107            gas: value.gas,
108            chain: Chain::from(value.chain),
109            tax: value.tax,
110            quality: value.quality,
111        })
112    }
113}
114
115/// Represents the quality of a token.
116///
117/// * `Good`: Indicates that the token has successfully passed the analysis process.
118/// * `Bad`: Indicates that the token has failed the analysis process. In this case, a detailed
119///   reason for the failure is provided.
120///
121/// Note: Transfer taxes do not impact the token's quality.
122/// Even if a token has transfer taxes, as long as it successfully passes the analysis,
123/// it will still be marked as `Good`.
124#[derive(Debug, Clone, Eq, PartialEq)]
125pub enum TokenQuality {
126    Good,
127    Bad { reason: String },
128}
129
130impl TokenQuality {
131    pub fn is_good(&self) -> bool {
132        matches!(self, Self::Good { .. })
133    }
134
135    pub fn bad(reason: impl ToString) -> Self {
136        Self::Bad { reason: reason.to_string() }
137    }
138}
139
140/// A store for tracking token owners and their balances.
141///
142/// The `TokenOwnerStore` maintains a mapping between token addresses and their respective
143/// owner's address and balance. It can be used to quickly retrieve token owner information
144/// without needing to query external sources.
145///
146/// # Fields
147/// * `values` - A `HashMap` where:
148///   * The key is the token `Address`, representing the address of the token being tracked.
149///   * The value is a tuple containing:
150///     * The owner `Address` of the token.
151///     * The `Balance` of the owner for the token.
152#[derive(Debug)]
153pub struct TokenOwnerStore {
154    values: HashMap<Address, (Address, Balance)>,
155}
156
157impl TokenOwnerStore {
158    pub fn new(values: HashMap<Address, (Address, Balance)>) -> Self {
159        TokenOwnerStore { values }
160    }
161}
162
163#[async_trait::async_trait]
164impl TokenOwnerFinding for TokenOwnerStore {
165    async fn find_owner(
166        &self,
167        token: Address,
168        _min_balance: Balance,
169    ) -> Result<Option<(Address, Balance)>, String> {
170        Ok(self.values.get(&token).cloned())
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use std::str::FromStr;
177
178    use super::*;
179
180    #[test]
181    fn test_constructor() {
182        let token = Token::new(
183            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
184            "USDC",
185            6,
186            1000,
187            &[Some(1000u64)],
188            Chain::Ethereum,
189            100,
190        );
191
192        assert_eq!(token.symbol, "USDC");
193        assert_eq!(token.decimals, 6);
194        assert_eq!(
195            format!("{token_address:#x}", token_address = token.address),
196            "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
197        );
198    }
199
200    #[test]
201    fn test_cmp() {
202        let usdc = Token::new(
203            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
204            "USDC",
205            6,
206            1000,
207            &[Some(1000u64)],
208            Chain::Ethereum,
209            100,
210        );
211        let usdc2 = Token::new(
212            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
213            "USDC2",
214            6,
215            1000,
216            &[Some(1000u64)],
217            Chain::Ethereum,
218            100,
219        );
220        let weth = Token::new(
221            &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
222            "WETH",
223            18,
224            1000,
225            &[Some(1000u64)],
226            Chain::Ethereum,
227            100,
228        );
229
230        assert!(usdc < weth);
231        assert_eq!(usdc, usdc2);
232    }
233
234    #[test]
235    fn test_one() {
236        let usdc = Token::new(
237            &Bytes::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(),
238            "USDC",
239            6,
240            1000,
241            &[Some(1000u64)],
242            Chain::Ethereum,
243            100,
244        );
245
246        assert_eq!(usdc.one(), BigUint::from(1000000u64));
247    }
248}