tycho_common/models/
token.rs

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