1use std::collections::{HashMap, HashSet};
2
3use alloy::{
4 primitives::{Address as AlloyAddress, B256},
5 rpc::types::{BlockId, BlockNumberOrTag},
6};
7use async_trait::async_trait;
8use chrono::DateTime;
9use futures::future::try_join_all;
10use tracing::{debug, info};
11use tycho_common::{
12 models::{blockchain::Block, contract::AccountDelta, Chain, ChangeType},
13 traits::{AccountExtractor, StorageSnapshotRequest},
14 Bytes,
15};
16
17use crate::{
18 rpc::{
19 errors::{RPCError, RequestError},
20 EthereumRpcClient,
21 },
22 BytesCodec,
23};
24
25pub struct EVMAccountExtractor {
31 rpc: EthereumRpcClient,
32 chain: Chain,
33}
34
35impl EVMAccountExtractor {
36 pub fn new(client: &EthereumRpcClient, chain: Chain) -> Self {
37 Self { rpc: client.clone(), chain }
39 }
40
41 pub async fn get_block_data(&self, block_id: u64) -> Result<Block, RPCError> {
42 let block_id = BlockId::from(block_id);
43
44 let block = self
45 .rpc
46 .eth_get_block_by_number(block_id)
47 .await?;
48
49 Ok(Block {
50 number: block.header.number,
51 hash: block.header.hash.to_bytes(),
52 parent_hash: block.header.parent_hash.to_bytes(),
53 chain: self.chain,
54 ts: DateTime::from_timestamp(block.header.timestamp as i64, 0)
55 .ok_or_else(|| {
56 RPCError::RequestError(RequestError::Other("Invalid timestamp in block".into()))
57 })?
58 .naive_utc(),
59 })
60 }
61}
62
63#[async_trait]
64impl AccountExtractor for EVMAccountExtractor {
65 type Error = RPCError;
66
67 async fn get_accounts_at_block(
68 &self,
69 block: &Block,
70 requests: &[StorageSnapshotRequest],
71 ) -> Result<HashMap<Bytes, AccountDelta>, Self::Error> {
72 let block_id = BlockNumberOrTag::Number(block.number);
73 let block_hash = B256::from_slice(&block.hash);
74
75 let mut updates = HashMap::new();
76
77 let unique_requests: Vec<StorageSnapshotRequest> = requests
79 .iter()
80 .cloned()
81 .collect::<HashSet<_>>()
82 .into_iter()
83 .collect();
84
85 let alloy_addresses: Vec<AlloyAddress> = unique_requests
87 .iter()
88 .map(|request| AlloyAddress::from_bytes(&request.address))
89 .collect();
90
91 let codes_and_balances_fut = self
93 .rpc
94 .fetch_accounts_code_and_balance(block_id, &alloy_addresses);
95
96 let storage_futs = unique_requests
98 .iter()
99 .map(|req| {
100 let address = AlloyAddress::from_bytes(&req.address);
101
102 let fut = async move {
103 if let Some(slots) = &req.slots {
104 let slots = slots
105 .iter()
106 .map(B256::from_bytes)
107 .collect::<Vec<_>>();
108
109 self.rpc
110 .get_selected_storage(block_id, address, &slots)
111 .await
112 } else {
113 self.rpc
114 .get_storage_range(address, block_hash)
115 .await
116 .map(|result| {
118 result
119 .into_iter()
120 .map(|(k, v)| (k, Some(v)))
121 .collect()
122 })
123 }
124 };
125
126 fut
127 })
128 .collect::<Vec<_>>();
129
130 let codes_and_balances = codes_and_balances_fut.await?;
131 debug!(block_number = block.number, "Successfully retrieved account code and balance data");
132
133 let storage_results = try_join_all(storage_futs).await?;
134 debug!(block_number = block.number, "Successfully retrieved account storage");
135
136 for (address, storage_result) in alloy_addresses
137 .iter()
138 .zip(storage_results)
139 {
140 let (code, balance) = codes_and_balances[address].clone();
141
142 let storage = storage_result
144 .into_iter()
145 .map(|(k, v)| (k.to_bytes(), v.map(|v| v.to_bytes())))
146 .collect::<HashMap<_, _>>();
147
148 let account_delta = AccountDelta::new(
149 self.chain,
150 address.to_bytes(),
151 storage,
152 Some(balance.to_bytes()),
153 Some(code),
154 ChangeType::Creation,
155 );
156
157 updates.insert(address.to_bytes(), account_delta);
158 }
159
160 info!(
161 total_accounts_processed = updates.len(),
162 block_number = block.number,
163 "Completed batch account extraction successfully"
164 );
165
166 Ok(updates)
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::str::FromStr;
173
174 use rstest::rstest;
175 use tracing::warn;
176 use tracing_test::traced_test;
177 use tycho_common::models::{Address, Chain};
178
179 use super::*;
180 use crate::test_fixtures::{
181 TestFixture, BALANCER_VAULT_EXPECTED_SLOTS, BALANCER_VAULT_STR, STETH_EXPECTED_SLOTS,
182 STETH_STR, TEST_SLOTS, TOKEN_ADDRESSES,
183 };
184
185 fn parse_address(address_str: &str) -> Address {
186 Address::from_str(address_str).expect("failed to parse address")
187 }
188
189 fn create_storage_request(
190 address_str: &str,
191 slots: Option<Vec<Bytes>>,
192 ) -> StorageSnapshotRequest {
193 StorageSnapshotRequest { address: parse_address(address_str), slots }
194 }
195
196 impl TestFixture {
197 fn create_evm_extractor(&self, batching: bool) -> EVMAccountExtractor {
198 let rpc_client = self.create_rpc_client(batching);
199
200 EVMAccountExtractor { rpc: rpc_client, chain: Chain::Ethereum }
201 }
202 }
203
204 #[rstest]
210 #[case(BALANCER_VAULT_STR, BALANCER_VAULT_EXPECTED_SLOTS)]
211 #[case(STETH_STR, STETH_EXPECTED_SLOTS)] #[traced_test]
213 #[tokio::test]
214 #[ignore = "require RPC connection"]
215 async fn test_account_extractor(
216 #[case] address_str: &str,
217 #[case] expected_slot_count: usize,
218 #[values(false, true)] batching: bool,
219 ) -> Result<(), RPCError> {
220 let fixture = TestFixture::new();
221 let extractor = fixture.create_evm_extractor(batching);
222
223 if expected_slot_count > 100_000 {
225 warn!(
226 "Testing large contract {} with {} storage slots - this will take ~2 minutes and retrieve ~50MB of data",
227 address_str, expected_slot_count
228 );
229 }
230
231 let requests = vec![create_storage_request(address_str, None)];
232
233 let updates = extractor
234 .get_accounts_at_block(&fixture.block, &requests)
235 .await?;
236
237 assert_eq!(updates.len(), 1, "Expected exactly 1 account update");
238
239 let update = updates
240 .get(&Bytes::from_str(address_str).expect("valid address"))
241 .expect("update exists");
242
243 assert_eq!(
244 update.slots.len(),
245 expected_slot_count,
246 "{} storage slot count mismatch. Expected: {}, Got: {}",
247 address_str,
248 expected_slot_count,
249 update.slots.len()
250 );
251
252 Ok(())
253 }
254
255 #[rstest]
256 #[traced_test]
257 #[tokio::test]
258 #[ignore = "require RPC connection"]
259 async fn test_get_storage_snapshots_plain(
260 #[values(false, true)] batching: bool,
261 ) -> Result<(), RPCError> {
262 let fixture = TestFixture::new();
263
264 let extractor = fixture.create_evm_extractor(batching);
265
266 let requests = vec![
267 create_storage_request(BALANCER_VAULT_STR, Some(vec![])),
268 create_storage_request(STETH_STR, Some(vec![])),
269 ];
270
271 let start_time = std::time::Instant::now();
272 let result = extractor
273 .get_accounts_at_block(&fixture.block, &requests)
274 .await?;
275 let duration = start_time.elapsed();
276 println!("Time taken to get storage snapshots: {duration:?}");
277
278 assert_eq!(result.len(), 2);
279
280 let first_address = parse_address(BALANCER_VAULT_STR);
282 let first_delta = result
283 .get(&first_address)
284 .expect("first address should exist");
285 assert_eq!(first_delta.address, first_address);
286 assert_eq!(first_delta.chain, Chain::Ethereum);
287 assert!(first_delta.code().is_some());
288 assert!(first_delta.balance.is_some());
289 println!("Balance: {:?}", first_delta.balance);
290
291 let second_address = parse_address(STETH_STR);
293 let second_delta: &AccountDelta = result
294 .get(&second_address)
295 .expect("second address should exist");
296 assert_eq!(second_delta.address, second_address);
297 assert_eq!(second_delta.chain, Chain::Ethereum);
298 assert!(second_delta.code().is_some());
299 assert!(second_delta.balance.is_some());
300 println!("Balance: {:?}", second_delta.balance);
301
302 Ok(())
303 }
304
305 #[rstest]
306 #[traced_test]
307 #[tokio::test]
308 #[ignore = "require RPC connection"]
309 async fn test_get_storage_snapshots_with_specific_slots(
310 #[values(false, true)] batching: bool,
311 ) -> Result<(), RPCError> {
312 let fixture = TestFixture::new();
313 let extractor = fixture.create_evm_extractor(batching);
314
315 let slots = &*TEST_SLOTS;
317 let slots_request = slots
318 .keys()
319 .map(|k| k.to_bytes())
320 .collect();
321
322 let requests = vec![create_storage_request(BALANCER_VAULT_STR, Some(slots_request))];
323
324 let result = extractor
325 .get_accounts_at_block(&fixture.block, &requests)
326 .await?;
327
328 assert_eq!(result.len(), 1);
329
330 let address = parse_address(BALANCER_VAULT_STR);
332 let delta = result
333 .get(&address)
334 .expect("address should exist");
335
336 assert_eq!(delta.address, address);
337 assert_eq!(delta.chain, Chain::Ethereum);
338 assert!(delta.code().is_some());
339 assert!(delta.balance.is_some());
340
341 assert_eq!(delta.slots.len(), 3);
343 for (key, value) in slots.iter() {
344 assert!(delta
345 .slots
346 .contains_key(&key.to_bytes()));
347 assert_eq!(
348 delta
349 .slots
350 .get(&key.to_bytes())
351 .and_then(|v| v.as_ref()),
352 Some(&value.to_bytes())
353 );
354 }
355
356 Ok(())
357 }
358
359 #[rstest]
360 #[traced_test]
361 #[tokio::test]
362 #[ignore = "require RPC connection"]
363 async fn test_get_storage_snapshots_with_empty_slot(
364 #[values(false, true)] batching: bool,
365 ) -> Result<(), RPCError> {
366 let fixture = TestFixture::new();
367 let extractor = fixture.create_evm_extractor(batching);
368
369 let slots_request: Vec<Bytes> = vec![Bytes::from_str(
371 "0000000000000000000000000000000000000000000000000000000000000002",
372 )
373 .unwrap()];
374
375 let requests =
376 vec![create_storage_request(BALANCER_VAULT_STR, Some(slots_request.clone()))];
377
378 let result = extractor
379 .get_accounts_at_block(&fixture.block, &requests)
380 .await?;
381
382 assert_eq!(result.len(), 1);
383
384 let address = parse_address(BALANCER_VAULT_STR);
386 let delta = result
387 .get(&address)
388 .expect("address should exist");
389
390 assert_eq!(delta.address, address);
391 assert_eq!(delta.chain, Chain::Ethereum);
392 assert!(delta.code().is_some());
393 assert!(delta.balance.is_some());
394
395 assert_eq!(delta.slots.len(), 1);
397 assert_eq!(
398 delta
399 .slots
400 .get(&slots_request[0])
401 .unwrap(),
402 &None
403 );
404
405 Ok(())
406 }
407
408 #[rstest]
409 #[traced_test]
410 #[tokio::test]
411 #[ignore = "require RPC connection"]
412 async fn test_get_storage_snapshots_multiple_accounts(
413 #[values(false, true)] batching: bool,
414 ) -> Result<(), RPCError> {
415 let fixture = TestFixture::new();
416 let extractor = fixture.create_evm_extractor(batching);
417
418 let requests: Vec<_> = TOKEN_ADDRESSES
420 .iter()
421 .map(|&addr| {
422 create_storage_request(
423 addr,
424 Some(vec![Bytes::from_str(
425 "0000000000000000000000000000000000000000000000000000000000000000",
426 )
427 .unwrap()]),
428 )
429 })
430 .collect();
431
432 let start_time = std::time::Instant::now();
433 let result = extractor
434 .get_accounts_at_block(&fixture.block, &requests)
435 .await?;
436 let duration = start_time.elapsed();
437 println!(
438 "Time taken to get storage snapshots for {} accounts: {:?}",
439 requests.len(),
440 duration
441 );
442
443 assert_eq!(result.len(), TOKEN_ADDRESSES.len());
444
445 for addr_str in TOKEN_ADDRESSES.iter() {
447 let address = parse_address(addr_str);
448 let delta = result
449 .get(&address)
450 .expect("address should exist");
451
452 assert_eq!(delta.address, address);
453 assert_eq!(delta.chain, Chain::Ethereum);
454 assert!(delta.code().is_some());
455 assert!(delta.balance.is_some());
456 assert_eq!(delta.slots.len(), 1);
457
458 println!(
459 "Address: {}, Code size: {}, Has balance: {}",
460 addr_str,
461 delta.code().as_ref().unwrap().len(),
462 delta.balance.is_some()
463 );
464 }
465
466 Ok(())
467 }
468}