tycho_common/traits.rs
1use core::fmt::Debug;
2use std::{collections::HashMap, sync::Arc};
3
4use async_trait::async_trait;
5
6use crate::{
7 models::{
8 blockchain::{
9 Block, BlockAggregatedChanges, BlockTag, EntryPointWithTracingParams, TracedEntryPoint,
10 TxInput,
11 },
12 contract::AccountDelta,
13 token::{Token, TokenQuality, TransferCost, TransferTax},
14 Address, Balance, BlockHash, StoreKey,
15 },
16 Bytes,
17};
18
19/// Indexes protocol state deltas from raw EVM transactions.
20///
21/// A `TxDeltaIndexer` is a native-code substitute for a Substreams package.
22/// It consumes a stream of finalised [`BlockAggregatedChanges`] to keep its internal
23/// protocol state current, then — on demand — applies a batch of in-flight
24/// [`TxInput`]s and returns the resulting [`BlockAggregatedChanges`]. The primary
25/// consumer is an Ethereum block builder that needs to know how a candidate
26/// transaction bundle alters DEX state before deciding whether to include it.
27///
28/// # Lifecycle
29///
30/// 1. **Hydrate** — call [`apply_block`][TxDeltaIndexer::apply_block] for each finalised block
31/// received from the Tycho client. The first call serves as initialisation: `state_deltas` and
32/// `component_balances` will contain the full snapshot at that height rather than a sparse
33/// delta. Subsequent calls apply incremental deltas.
34///
35/// 2. **Query** — call [`generate_deltas`][TxDeltaIndexer::generate_deltas] with a batch of
36/// in-flight transactions at any point. The indexer applies them against its current internal
37/// state and returns a [`BlockAggregatedChanges`] describing what would change. Internal state
38/// is **not** mutated by this call; it always operates on the state left by the most recent
39/// `apply_block`.
40pub trait TxDeltaIndexer: Send {
41 /// Advances internal protocol state by applying a finalised block.
42 ///
43 /// Must be called in block-height order. The first call initialises the
44 /// indexer from a full snapshot; subsequent calls apply incremental state
45 /// deltas. After this call returns, [`generate_deltas`][TxDeltaIndexer::generate_deltas]
46 /// will produce deltas relative to the new state.
47 ///
48 /// # Parameters
49 ///
50 /// * `block` — a finalised [`BlockAggregatedChanges`] as received from the Tycho client. On the
51 /// first call this carries the full component and state snapshot; on later calls it carries
52 /// only the changed attributes and balances.
53 fn apply_block(&mut self, block: &BlockAggregatedChanges) -> anyhow::Result<()>;
54
55 /// Applies a batch of in-flight transactions against the current state and
56 /// returns the protocol state deltas they would produce.
57 ///
58 /// The returned [`BlockAggregatedChanges`] contains the aggregated deltas across
59 /// all transactions in the batch: `state_deltas`, `component_balances`,
60 /// `new_protocol_components`, and `deleted_protocol_components`. Block
61 /// metadata (`block`, `chain`, `extractor`, `finalized_block_height`) is
62 /// populated from the state stored by the most recent
63 /// [`apply_block`][TxDeltaIndexer::apply_block] call.
64 ///
65 /// Internal state is **not** modified. Calling `generate_deltas` twice with
66 /// the same transactions returns identical results.
67 ///
68 /// Transactions where `succeeded == false` are silently skipped.
69 ///
70 /// # Parameters
71 ///
72 /// * `txs` — ordered slice of in-flight transactions, typically a builder's candidate bundle or
73 /// the full mempool selection for one block.
74 fn generate_deltas(&mut self, txs: &[TxInput]) -> BlockAggregatedChanges;
75}
76
77/// A struct representing a request to get an account state.
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct StorageSnapshotRequest {
80 // The address of the account to get the state of.
81 pub address: Address,
82 // The specific slots to get the state of. If `None`, the entire account state will be
83 // returned.
84 pub slots: Option<Vec<StoreKey>>,
85}
86
87impl std::fmt::Display for StorageSnapshotRequest {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 let address_str = self.address.to_string();
90 let truncated_address = if address_str.len() >= 10 {
91 format!("{}...{}", &address_str[0..8], &address_str[address_str.len() - 4..])
92 } else {
93 address_str
94 };
95
96 match &self.slots {
97 Some(slots) => write!(f, "{truncated_address}[{} slots]", slots.len()),
98 None => write!(f, "{truncated_address}[all slots]"),
99 }
100 }
101}
102
103/// Trait for getting multiple account states from chain data.
104#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
105#[async_trait]
106pub trait AccountExtractor {
107 type Error: Debug + Send + Sync;
108
109 /// Get the account states at the end of the given block (after all transactions in the block
110 /// have been applied).
111 ///
112 /// # Arguments
113 ///
114 /// * `block`: The block at which to retrieve the account states.
115 /// * `requests`: A slice of `StorageSnapshotRequest` objects, each containing an address and
116 /// optional slots.
117 /// Note: If the `slots` field is `None`, the function will return the entire account state.
118 /// That could be a lot of data, so use with caution.
119 ///
120 /// returns: Result<HashMap<Bytes, AccountDelta, RandomState>, Self::Error>
121 /// A result containing a HashMap where the keys are `Bytes` (addresses) and the values are
122 /// `AccountDelta` objects.
123 async fn get_accounts_at_block(
124 &self,
125 block: &Block,
126 requests: &[StorageSnapshotRequest],
127 ) -> Result<HashMap<Bytes, AccountDelta>, Self::Error>; //TODO: do not return `AccountUpdate` but `Account`
128}
129
130/// Trait for analyzing a token, including its quality, transfer cost, and transfer tax.
131#[async_trait]
132pub trait TokenAnalyzer: Send + Sync {
133 type Error;
134
135 /// Analyzes the quality of a token given its address and a block tag.
136 ///
137 /// # Parameters
138 /// * `token` - The address of the token to analyze.
139 /// * `block` - The block tag at which the analysis should be performed.
140 ///
141 /// # Returns
142 /// A result containing:
143 /// * `TokenQuality` - The quality assessment of the token (either `Good` or `Bad`).
144 /// * `Option<TransferCost>` - The average cost per transfer, if available.
145 /// * `Option<TransferTax>` - The transfer tax, if applicable.
146 ///
147 /// On failure, returns `Self::Error`.
148 async fn analyze(
149 &self,
150 token: Bytes,
151 block: BlockTag,
152 ) -> Result<(TokenQuality, Option<TransferCost>, Option<TransferTax>), Self::Error>;
153}
154
155/// Trait for finding an address that owns a specific token. This is useful for detecting
156/// bad tokens by identifying addresses with enough balance to simulate transactions.
157#[async_trait]
158pub trait TokenOwnerFinding: Send + Sync + Debug {
159 /// Finds an address that holds at least `min_balance` of the specified token.
160 ///
161 /// # Parameters
162 /// * `token` - The address of the token to search for.
163 /// * `min_balance` - The minimum balance required for the address to be considered.
164 ///
165 /// # Returns
166 /// A result containing:
167 /// * `Option<(Address, Balance)>` - The address and its actual balance if an owner is found.
168 /// If no address meets the criteria, returns `None`.
169 /// On failure, returns a string representing an error message.
170 async fn find_owner(
171 &self,
172 token: Address,
173 min_balance: Balance,
174 ) -> Result<Option<(Address, Balance)>, String>; // TODO: introduce custom error type
175}
176
177/// Trait for retrieving additional information about tokens, such as the number of decimals
178/// and the token symbol, to help construct `CurrencyToken` objects.
179#[async_trait]
180pub trait TokenPreProcessor: Send + Sync {
181 /// Given a list of token addresses, this function retrieves additional metadata for each token.
182 ///
183 /// # Parameters
184 /// * `addresses` - A vector of token addresses to process.
185 /// * `token_finder` - A reference to a `TokenOwnerFinding` implementation to help find token
186 /// owners.
187 /// * `block` - The block tag at which the information should be retrieved.
188 ///
189 /// # Returns
190 /// A vector of `CurrencyToken` objects, each containing the processed information for the
191 /// token.
192 async fn get_tokens(
193 &self,
194 addresses: Vec<Bytes>,
195 token_finder: Arc<dyn TokenOwnerFinding>,
196 block: BlockTag,
197 ) -> Vec<Token>;
198}
199
200/// Trait for tracing blockchain transaction execution.
201#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
202#[async_trait]
203pub trait EntryPointTracer: Sync {
204 type Error: Debug;
205
206 /// Traces the execution of a list of entry points at a specific block.
207 ///
208 /// # Parameters
209 /// * `block_hash` - The hash of the block at which to perform the trace. The trace will use the
210 /// state of the blockchain at this block.
211 /// * `entry_points` - A list of entry points to trace with their data.
212 ///
213 /// # Returns
214 /// Returns a vector of `TracedEntryPoint`, where each element contains:
215 /// * `retriggers` - A set of (address, storage slot) pairs representing storage locations that
216 /// could alter tracing results. If any of these storage slots change, the set of called
217 /// contract might be outdated.
218 /// * `accessed_slots` - A map of all contract addresses that were called during the trace with
219 /// a list of storage slots that were accessed (read or written).
220 async fn trace(
221 &self,
222 block_hash: BlockHash,
223 entry_points: Vec<EntryPointWithTracingParams>,
224 ) -> Vec<Result<TracedEntryPoint, Self::Error>>;
225}
226
227/// Trait for detecting storage slots that contain ERC20 token balances
228#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
229#[async_trait]
230pub trait BalanceSlotDetector: Send + Sync {
231 type Error: Debug;
232
233 /// Detect balance storage slots for multiple tokens from a single holder.
234 /// Useful to allow overriding balances.
235 ///
236 /// # Arguments
237 /// * `tokens` - Slice of ERC20 token addresses.
238 /// * `holder` - Address that holds the tokens (e.g., pool manager)
239 /// * `block_hash` - Block at which to detect slots
240 ///
241 /// # Returns
242 /// HashMap mapping Token -> Result containing (contract_address -> storage_slot) or error.
243 /// The storage slot is the one that controls the token's holder balance.
244 async fn detect_balance_slots(
245 &self,
246 tokens: &[Address],
247 holder: Address,
248 block_hash: BlockHash,
249 ) -> HashMap<Address, Result<(Address, Bytes), Self::Error>>;
250}
251
252/// Trait for detecting storage slots that contain ERC20 token allowances
253#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
254#[async_trait]
255pub trait AllowanceSlotDetector: Send + Sync {
256 type Error: Debug;
257
258 /// Detect allowance storage slots for multiple tokens for owner-spender pairs.
259 /// Useful to allow overriding allowances in simulations.
260 ///
261 /// # Arguments
262 /// * `tokens` - Slice of ERC20 token addresses.
263 /// * `owner` - Address that owns the tokens
264 /// * `spender` - Address that is allowed to spend the tokens
265 /// * `block_hash` - Block at which to detect slots
266 ///
267 /// # Returns
268 /// HashMap mapping Token -> Result containing (contract_address -> storage_slot) or error.
269 /// The storage slot is the one that controls the allowance from owner to spender.
270 async fn detect_allowance_slots(
271 &self,
272 tokens: &[Address],
273 owner: Address,
274 spender: Address,
275 block_hash: BlockHash,
276 ) -> HashMap<Address, Result<(Address, Bytes), Self::Error>>;
277}
278
279/// Trait for getting the transaction fee price from the node.
280/// The fee price is the dynamic part of the fee, which usually varies based on the network
281/// congestion. For example, on Ethereum, the fee price is the gas price (base fee + priority fee).
282#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String; type FeePrice = u128;))]
283#[async_trait]
284pub trait FeePriceGetter: Send + Sync {
285 type Error: Debug;
286 type FeePrice;
287
288 /// Get the latest fee price information from the chain.
289 ///
290 /// # Returns
291 /// A chain-specific fee price type that can provide an effective fee price.
292 async fn get_latest_fee_price(&self) -> Result<Self::FeePrice, Self::Error>;
293}
294
295#[cfg(test)]
296mod tests {
297 use std::str::FromStr;
298
299 use super::*;
300
301 #[test]
302 fn test_storage_snapshot_request_display() {
303 // Test with specific slots
304 let request_with_slots = StorageSnapshotRequest {
305 address: Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
306 slots: Some(vec![
307 StoreKey::from(vec![1, 2, 3, 4]),
308 StoreKey::from(vec![5, 6, 7, 8]),
309 StoreKey::from(vec![9, 10, 11, 12]),
310 ]),
311 };
312
313 let display_output = request_with_slots.to_string();
314 assert_eq!(display_output, "0x123456...7890[3 slots]");
315
316 // Test with all slots
317 let request_all_slots = StorageSnapshotRequest {
318 address: Address::from_str("0x9876543210987654321098765432109876543210").unwrap(),
319 slots: None,
320 };
321
322 let display_output = request_all_slots.to_string();
323 assert_eq!(display_output, "0x987654...3210[all slots]");
324
325 // Test with empty slots vector
326 let request_empty_slots = StorageSnapshotRequest {
327 address: Address::from_str("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd").unwrap(),
328 slots: Some(vec![]),
329 };
330
331 let display_output = request_empty_slots.to_string();
332 assert_eq!(display_output, "0xabcdef...abcd[0 slots]");
333 }
334}