Skip to main content

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