1use std::sync::Arc;
2
3use alloy::{primitives::Address, rpc::types::BlockNumberOrTag, sol_types::SolCall};
4use async_trait::async_trait;
5use tracing::{instrument, warn};
6use tycho_common::{
7 models::{
8 blockchain::BlockTag,
9 token::{Token, TokenQuality},
10 Chain,
11 },
12 traits::{TokenAnalyzer, TokenOwnerFinding, TokenPreProcessor},
13 Bytes,
14};
15use unicode_segmentation::UnicodeSegmentation;
16
17use crate::{
18 erc20::{decimalsCall, symbolCall},
19 rpc::EthereumRpcClient,
20 services::token_analyzer::{call_request, EthCallDetector},
21 BytesCodec,
22};
23
24#[derive(Debug, Clone)]
25pub struct EthereumTokenPreProcessor {
26 rpc: EthereumRpcClient,
27 chain: Chain,
28 settlement_contract: Address,
29}
30
31impl EthereumTokenPreProcessor {
32 pub fn new(rpc: &EthereumRpcClient, chain: Chain, settlement_contract: Address) -> Self {
33 EthereumTokenPreProcessor { rpc: rpc.clone(), chain, settlement_contract }
34 }
35
36 async fn call_symbol(&self, token: Address) -> String {
37 let calldata = symbolCall {}.abi_encode();
38
39 let result = match self
40 .rpc
41 .eth_call(call_request(None, token, calldata), BlockNumberOrTag::Latest)
42 .await
43 {
44 Ok(result) => result,
45 Err(e) => {
46 warn!(?e, ?token, "Failed to call symbol function, using address as fallback");
47 return format!("0x{:x}", token);
48 }
49 };
50
51 match symbolCall::abi_decode_returns_validate(&result) {
52 Ok(symbol) => symbol,
53 Err(e) => {
54 warn!(
55 ?e,
56 ?token,
57 "Failed to decode symbol function result, using address as fallback"
58 );
59 format!("0x{:x}", token)
60 }
61 }
62 }
63
64 async fn call_decimals(&self, token: Address) -> u8 {
65 let calldata = decimalsCall {}.abi_encode();
66
67 let result = match self
68 .rpc
69 .eth_call(call_request(None, token, calldata), BlockNumberOrTag::Latest)
70 .await
71 {
72 Ok(result) => result,
73 Err(e) => {
74 warn!(?e, ?token, "Failed to call decimals function, using default decimals 18");
75 return 18;
76 }
77 };
78
79 match decimalsCall::abi_decode_returns_validate(&result) {
80 Ok(decimals) => decimals,
81 Err(e) => {
82 warn!(
83 ?e,
84 ?token,
85 "Failed to decode decimals function result, using default decimals 18"
86 );
87 18
88 }
89 }
90 }
91}
92
93#[async_trait]
94impl TokenPreProcessor for EthereumTokenPreProcessor {
95 #[instrument(skip_all, fields(n_addresses=addresses.len(), block = ?block))]
96 async fn get_tokens(
97 &self,
98 addresses: Vec<Bytes>,
99 token_finder: Arc<dyn TokenOwnerFinding>,
100 block: BlockTag,
101 ) -> Vec<Token> {
102 let mut tokens_info = Vec::new();
103
104 for address in addresses {
105 let token_address = Address::from_bytes(&address);
106
107 let symbol = self.call_symbol(token_address).await;
109 let decimals = self.call_decimals(token_address).await;
110
111 let detector =
112 EthCallDetector::new(&self.rpc, token_finder.clone(), self.settlement_contract);
113
114 let (token_quality, gas, tax) = detector
115 .analyze(address.clone(), block)
116 .await
117 .unwrap_or_else(|e| {
118 warn!(error=?e, "TokenDetectionFailure");
119 (TokenQuality::bad("Detection failed"), None, None)
120 });
121
122 let mut quality = 100;
123
124 if let TokenQuality::Bad { reason } = token_quality {
125 warn!(address=?address, ?reason, "BadToken");
126 quality = 10;
129 };
130
131 if quality == 100 && tax.is_some_and(|tax_value| tax_value > 0) {
133 quality = 50;
134 }
135
136 tokens_info.push(Token {
137 address,
138 symbol: symbol
139 .replace('\0', "")
140 .graphemes(true)
141 .take(255)
142 .collect::<String>(),
143 decimals: decimals.into(),
144 tax: tax.unwrap_or(0),
145 gas: gas
146 .map(|g| vec![Some(g)])
147 .unwrap_or_else(Vec::new),
148 chain: self.chain,
149 quality,
150 });
151 }
152
153 tokens_info
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use std::str::FromStr;
160
161 use alloy::primitives::address;
162 use tycho_common::models::token::TokenOwnerStore;
163
164 use super::*;
165 use crate::test_fixtures::{TestFixture, TEST_BLOCK_NUMBER, TOKEN_HOLDERS, USDC_STR, WETH_STR};
166
167 const COWSWAP_SETTLEMENT: Address = address!("c9f2e6ea1637E499406986ac50ddC92401ce1f58");
168
169 impl TestFixture {
170 fn create_token_preprocessor(&self) -> EthereumTokenPreProcessor {
171 let rpc = self.create_rpc_client(false);
173
174 EthereumTokenPreProcessor::new(&rpc, Chain::Ethereum, COWSWAP_SETTLEMENT)
175 }
176 }
177
178 #[tokio::test]
179 #[ignore = "require RPC connection"]
180 async fn test_call_symbol() {
181 let fixture = TestFixture::new();
182 let processor = fixture.create_token_preprocessor();
183
184 let weth_address = Address::from_str(WETH_STR).expect("Failed to parse WETH address");
186 let symbol = processor
187 .call_symbol(weth_address)
188 .await;
189 assert_eq!(symbol, "WETH", "Expected WETH symbol");
190
191 let usdc_address = Address::from_str(USDC_STR).expect("Failed to parse USDC address");
193 let symbol = processor
194 .call_symbol(usdc_address)
195 .await;
196 assert_eq!(symbol, "USDC", "Expected USDC symbol");
197 }
198
199 #[tokio::test]
200 #[ignore = "require RPC connection"]
201 async fn test_call_decimals() {
202 let fixture = TestFixture::new();
203 let processor = fixture.create_token_preprocessor();
204
205 let weth_address = Address::from_str(WETH_STR).expect("Failed to parse WETH address");
207 let decimals = processor
208 .call_decimals(weth_address)
209 .await;
210 assert_eq!(decimals, 18, "Expected WETH to have 18 decimals");
211
212 let usdc_address = Address::from_str(USDC_STR).expect("Failed to parse USDC address");
214 let decimals = processor
215 .call_decimals(usdc_address)
216 .await;
217 assert_eq!(decimals, 6, "Expected USDC to have 6 decimals");
218 }
219
220 #[tokio::test]
221 #[ignore = "require archive RPC connection"]
222 async fn test_get_tokens() {
223 let fixture = TestFixture::new();
224 let processor = fixture.create_token_preprocessor();
225
226 let tf = TokenOwnerStore::new(TOKEN_HOLDERS.clone());
227
228 let fake_address: &str = "0xA0b86991c7456b36c1d19D4a2e9Eb0cE3606eB48";
229 let addresses = vec![
230 Bytes::from_str(WETH_STR).unwrap(),
231 Bytes::from_str(USDC_STR).unwrap(),
232 Bytes::from_str(fake_address).unwrap(),
233 ];
234
235 let results = processor
236 .get_tokens(addresses, Arc::new(tf), BlockTag::Number(TEST_BLOCK_NUMBER))
237 .await;
238 assert_eq!(results.len(), 3);
239 let relevant_attrs: Vec<(String, u32, u32)> = results
240 .iter()
241 .map(|t| (t.symbol.clone(), t.decimals, t.quality))
242 .collect();
243 assert_eq!(
244 relevant_attrs,
245 vec![
246 ("WETH".to_string(), 18, 100),
247 ("USDC".to_string(), 6, 100),
248 (fake_address.to_lowercase(), 18, 10)
249 ]
250 );
251 }
252}