Skip to main content

tycho_simulation/evm/
decoder.rs

1use std::{
2    collections::{hash_map::Entry, HashMap, HashSet},
3    future::Future,
4    pin::Pin,
5    sync::Arc,
6};
7
8use alloy::primitives::{Address, U256};
9use thiserror::Error;
10use tokio::sync::{watch, RwLock, RwLockReadGuard};
11use tracing::{debug, error, info, warn};
12use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader, FeedMessage, HeaderLike};
13use tycho_common::{
14    dto::{ChangeType, ProtocolStateDelta},
15    models::{blockchain::BlockAggregatedChanges, token::Token, Chain},
16    simulation::protocol_sim::{Balances, ProtocolSim},
17    Bytes,
18};
19#[cfg(test)]
20use {
21    mockall::mock,
22    num_bigint::BigUint,
23    std::any::Any,
24    tycho_common::simulation::{
25        errors::{SimulationError, TransitionError},
26        protocol_sim::GetAmountOutResult,
27    },
28};
29
30use crate::{
31    evm::{
32        engine_db::{update_engine, SHARED_TYCHO_DB},
33        override_stream::{OverrideSnapshot, StateOverrideProvider},
34        protocol::{
35            utils::bytes_to_address,
36            vm::{constants::ERC20_PROXY_BYTECODE, erc20_token::IMPLEMENTATION_SLOT},
37        },
38        tycho_models::{AccountUpdate, ResponseAccount},
39    },
40    protocol::{
41        errors::InvalidSnapshotError,
42        models::{DecoderContext, ProtocolComponent, TryFromWithBlock, Update},
43    },
44};
45
46#[derive(Error, Debug)]
47pub enum StreamDecodeError {
48    #[error("{0}")]
49    Fatal(String),
50}
51
52#[derive(Default)]
53struct DecoderState {
54    tokens: HashMap<Bytes, Token>,
55    states: HashMap<String, Box<dyn ProtocolSim>>,
56    components: HashMap<String, ProtocolComponent>,
57    // maps contract address to the pools they affect
58    contracts_map: HashMap<Bytes, HashSet<String>>,
59    // Maps original token address to their new proxy token address
60    proxy_token_addresses: HashMap<Address, Address>,
61    // Set of failed components, these are components that failed to decode and will not be emitted
62    // again TODO: handle more gracefully inside tycho-client. We could fetch the snapshot and
63    // try to decode it again.
64    failed_components: HashSet<String>,
65    // The block number of the last confirmed block decoded via `decode()`.
66    current_block_number: u64,
67}
68
69type DecodeFut =
70    Pin<Box<dyn Future<Output = Result<Box<dyn ProtocolSim>, InvalidSnapshotError>> + Send + Sync>>;
71type AccountBalances = HashMap<Bytes, HashMap<Bytes, Bytes>>;
72type RegistryFn<H> = dyn Fn(
73        ComponentWithState,
74        H,
75        AccountBalances,
76        Arc<RwLock<DecoderState>>,
77        Option<watch::Receiver<OverrideSnapshot>>,
78    ) -> DecodeFut
79    + Send
80    + Sync;
81type FilterFn = fn(&ComponentWithState) -> bool;
82
83/// A decoder to process raw messages.
84///
85/// This struct decodes incoming messages of type `FeedMessage` and converts it into the
86/// `BlockUpdate` struct.
87///
88/// # Important:
89/// - Supports registering exchanges and their associated filters for specific protocol components.
90/// - Allows the addition of client-side filters for custom conditions.
91///
92/// **Note:** Tokens provided via [`set_tokens`](Self::set_tokens) are used to decode startup
93/// snapshots and initialize protocol states. This is not an ongoing filter — components arriving
94/// after startup include their own token metadata.
95pub struct TychoStreamDecoder<H>
96where
97    H: HeaderLike,
98{
99    state: Arc<RwLock<DecoderState>>,
100    skip_state_decode_failures: bool,
101    min_token_quality: u32,
102    registry: HashMap<String, Box<RegistryFn<H>>>,
103    inclusion_filters: HashMap<String, FilterFn>,
104    /// Live override providers keyed by `protocol_system`. A pool of that protocol subscribes to
105    /// its provider at creation time and reads fresh overrides on every simulation.
106    override_providers: HashMap<String, Arc<dyn StateOverrideProvider>>,
107}
108
109impl<H> Default for TychoStreamDecoder<H>
110where
111    H: HeaderLike + Clone + Sync + Send + 'static + std::fmt::Debug,
112{
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118/// Curve migrated from the generic VM adapter (`EVMPoolState`) to the native [`CurveState`]
119/// decoder. Returns true when `vm:curve` is registered with any other type — i.e. the deprecated
120/// VM-adapter path, still supported for a few releases before removal.
121fn is_deprecated_curve_registration<T: 'static>(exchange: &str) -> bool {
122    exchange == "vm:curve" &&
123        std::any::type_name::<T>() !=
124            std::any::type_name::<crate::evm::protocol::curve::CurveState>()
125}
126
127impl<H> TychoStreamDecoder<H>
128where
129    H: HeaderLike + Clone + Sync + Send + 'static + std::fmt::Debug,
130{
131    pub fn new() -> Self {
132        Self {
133            state: Arc::new(RwLock::new(DecoderState::default())),
134            skip_state_decode_failures: false,
135            min_token_quality: 100,
136            registry: HashMap::new(),
137            inclusion_filters: HashMap::new(),
138            override_providers: HashMap::new(),
139        }
140    }
141
142    /// Registers `provider` as the live override source for `protocol_system`.
143    ///
144    /// Pools of that protocol subscribe to it at creation time, so overrides apply from the first
145    /// simulation onward. A later call for the same `protocol_system` replaces the previous
146    /// provider.
147    pub fn set_override_provider(
148        &mut self,
149        protocol_system: String,
150        provider: Arc<dyn StateOverrideProvider>,
151    ) {
152        self.override_providers
153            .insert(protocol_system, provider);
154    }
155
156    /// Provides token metadata used to decode startup snapshots and initialize protocol states.
157    ///
158    /// This is not an ongoing stream filter. Components arriving after startup include their
159    /// own token metadata for decoding.
160    pub async fn set_tokens(&self, tokens: HashMap<Bytes, Token>) {
161        let mut guard = self.state.write().await;
162        guard.tokens = tokens;
163    }
164
165    pub fn skip_state_decode_failures(&mut self, skip: bool) {
166        self.skip_state_decode_failures = skip;
167    }
168
169    /// Sets the minimum token quality for decoding.
170    ///
171    /// Tokens arriving in stream deltas below this threshold are ignored. Defaults to 100.
172    /// Set this to the same value used in [`load_all_tokens()`](crate::utils::load_all_tokens) to
173    /// apply consistent filtering.
174    pub fn min_token_quality(&mut self, quality: u32) {
175        self.min_token_quality = quality;
176    }
177
178    /// Registers a decoder for a given exchange with a decoder context.
179    ///
180    /// This method maps an exchange identifier to a specific protocol simulation type.
181    /// The associated type must implement the `TryFromWithBlock` trait to enable decoding
182    /// of state updates from `ComponentWithState` objects. This allows the decoder to transform
183    /// the component data into the appropriate protocol simulation type based on the current
184    /// blockchain state and the provided block header.
185    /// For example, to register a decoder for the `uniswap_v2` exchange with an additional decoder
186    /// context, you must call this function with
187    /// `register_decoder_with_context::<UniswapV2State>("uniswap_v2", context)`.
188    /// This ensures that the exchange ID `uniswap_v2` is properly associated with the
189    /// `UniswapV2State` decoder for use in the protocol stream.
190    pub fn register_decoder_with_context<T>(&mut self, exchange: &str, context: DecoderContext)
191    where
192        T: ProtocolSim
193            + TryFromWithBlock<ComponentWithState, H, Error = InvalidSnapshotError>
194            + Send
195            + 'static,
196    {
197        if is_deprecated_curve_registration::<T>(exchange) {
198            warn!(
199                registered_type = std::any::type_name::<T>(),
200                "Registering \"vm:curve\" with the generic VM adapter is deprecated; register the \
201                 native `CurveState` decoder instead (`exchange::<CurveState>(\"vm:curve\", ...)`). \
202                 The VM-adapter path still works but will be removed in a future release."
203            );
204        }
205        let decoder = Box::new(
206            move |component: ComponentWithState,
207                  header: H,
208                  account_balances: AccountBalances,
209                  state: Arc<RwLock<DecoderState>>,
210                  live_override: Option<watch::Receiver<OverrideSnapshot>>| {
211                let mut context = context.clone();
212                context.live_override = live_override;
213                Box::pin(async move {
214                    let guard = state.read().await;
215                    T::try_from_with_header(
216                        component,
217                        header,
218                        &account_balances,
219                        &guard.tokens,
220                        &context,
221                    )
222                    .await
223                    .map(|c| Box::new(c) as Box<dyn ProtocolSim>)
224                }) as DecodeFut
225            },
226        );
227        self.registry
228            .insert(exchange.to_string(), decoder);
229    }
230
231    /// Registers a decoder for a given exchange.
232    ///
233    /// This method maps an exchange identifier to a specific protocol simulation type.
234    /// The associated type must implement the `TryFromWithBlock` trait to enable decoding
235    /// of state updates from `ComponentWithState` objects. This allows the decoder to transform
236    /// the component data into the appropriate protocol simulation type based on the current
237    /// blockchain state and the provided block header.
238    /// For example, to register a decoder for the `uniswap_v2` exchange, you must call
239    /// this function with `register_decoder::<UniswapV2State>("uniswap_v2", vm_attributes)`.
240    /// This ensures that the exchange ID `uniswap_v2` is properly associated with the
241    /// `UniswapV2State` decoder for use in the protocol stream.
242    pub fn register_decoder<T>(&mut self, exchange: &str)
243    where
244        T: ProtocolSim
245            + TryFromWithBlock<ComponentWithState, H, Error = InvalidSnapshotError>
246            + Send
247            + 'static,
248    {
249        let context = DecoderContext::new();
250        self.register_decoder_with_context::<T>(exchange, context);
251    }
252
253    /// Registers a client-side filter function for a given exchange.
254    ///
255    /// Associates a filter function with an exchange ID, enabling custom filtering of protocol
256    /// components. The filter function is applied client-side to refine the data received from the
257    /// stream. It can be used to exclude certain components based on attributes or conditions that
258    /// are not supported by the server-side filtering logic. This is particularly useful for
259    /// implementing custom behaviors, such as:
260    /// - Filtering out pools with specific attributes (e.g., unsupported features).
261    /// - Blacklisting pools based on custom criteria.
262    /// - Excluding pools that do not meet certain requirements (e.g., token pairs or liquidity
263    ///   constraints).
264    ///
265    /// For example, you might use a filter to exclude pools that are not fully supported in the
266    /// protocol, or to ignore pools with certain attributes that are irrelevant to your
267    /// application.
268    pub fn register_filter(&mut self, exchange: &str, predicate: FilterFn) {
269        self.inclusion_filters
270            .insert(exchange.to_string(), predicate);
271    }
272
273    /// Decodes a `FeedMessage` into a `BlockUpdate` containing the updated states of protocol
274    /// components
275    pub async fn decode(&self, msg: &FeedMessage<H>) -> Result<Update, StreamDecodeError> {
276        // stores all states updated in this tick/msg
277        let mut updated_states = HashMap::new();
278        let mut new_pairs = HashMap::new();
279        let mut removed_pairs = HashMap::new();
280        let mut contracts_map = HashMap::new();
281        let mut msg_failed_components = HashSet::new();
282
283        let header = msg
284            .state_msgs
285            .values()
286            .next()
287            .ok_or_else(|| StreamDecodeError::Fatal("Missing block!".into()))?
288            .header
289            .clone();
290
291        let block_number_or_timestamp = header
292            .clone()
293            .block_number_or_timestamp();
294        let current_block = header.clone().block();
295        let is_partial = current_block
296            .as_ref()
297            .map(|h| h.partial_block_index.is_some())
298            .unwrap_or(false);
299
300        for (protocol, protocol_msg) in msg.state_msgs.iter() {
301            // Add any new tokens
302            if let Some(deltas) = protocol_msg.deltas.as_ref() {
303                let mut state_guard = self.state.write().await;
304
305                let new_tokens = deltas
306                    .new_tokens
307                    .iter()
308                    .filter(|(addr, t)| {
309                        t.quality >= self.min_token_quality &&
310                            !state_guard.tokens.contains_key(*addr)
311                    })
312                    .map(|(addr, t)| (addr.clone(), t.clone()))
313                    .collect::<HashMap<Bytes, Token>>();
314
315                if !new_tokens.is_empty() {
316                    debug!(n = new_tokens.len(), "NewTokens");
317                    state_guard.tokens.extend(new_tokens);
318                }
319            }
320
321            // Remove untracked components
322            {
323                let mut state_guard = self.state.write().await;
324                let removed_components: Vec<(String, ProtocolComponent)> = protocol_msg
325                    .removed_components
326                    .iter()
327                    .map(|(id, comp)| {
328                        if *id != comp.id {
329                            error!(
330                                "Component id mismatch in removed components {id} != {}",
331                                comp.id
332                            );
333                            return Err(StreamDecodeError::Fatal("Component id mismatch".into()));
334                        }
335
336                        let tokens = comp
337                            .tokens
338                            .iter()
339                            .flat_map(|addr| state_guard.tokens.get(addr).cloned())
340                            .collect::<Vec<_>>();
341
342                        if tokens.len() == comp.tokens.len() {
343                            Ok(Some((
344                                id.clone(),
345                                ProtocolComponent::from_with_tokens(comp.clone(), tokens),
346                            )))
347                        } else {
348                            Ok(None)
349                        }
350                    })
351                    .collect::<Result<Vec<Option<(String, ProtocolComponent)>>, StreamDecodeError>>(
352                    )?
353                    .into_iter()
354                    .flatten()
355                    .collect();
356
357                // Remove components from state and add to removed_pairs
358                for (id, component) in removed_components {
359                    state_guard.components.remove(&id);
360                    state_guard.states.remove(&id);
361                    removed_pairs.insert(id, component);
362                }
363
364                // UPDATE VM STORAGE
365                info!(
366                    "Processing {} contracts from snapshots",
367                    protocol_msg
368                        .snapshots
369                        .get_vm_storage()
370                        .len()
371                );
372
373                let mut proxy_token_accounts: HashMap<Address, AccountUpdate> = HashMap::new();
374                let mut storage_by_address: HashMap<Address, ResponseAccount> = HashMap::new();
375                for (key, value) in protocol_msg
376                    .snapshots
377                    .get_vm_storage()
378                    .iter()
379                {
380                    let account: ResponseAccount = value.clone().into();
381
382                    if state_guard.tokens.contains_key(key) {
383                        let original_address = account.address;
384                        // To work with Tycho's token overwrites system, if we get account
385                        // snapshots for a token we must handle them with a proxy/wrapper
386                        // contract.
387                        // Note: storage for the original contract must be set at the proxy
388                        // contract address. This is because the proxy contract uses
389                        // delegatecall to the original (implementation) contract.
390
391                        // Handle proxy token accounts
392                        let (impl_addr, proxy_state) = match state_guard
393                            .proxy_token_addresses
394                            .get(&original_address)
395                        {
396                            Some(impl_addr) => {
397                                // Token already has a proxy contract, simply update it.
398
399                                // Note: we apply the snapshot as an update. This is to cover the
400                                // case where a contract may be stale as it stopped being tracked
401                                // for some reason (e.g. due to a drop in tvl) and is now being
402                                // tracked again.
403                                let proxy_state = AccountUpdate::new(
404                                    original_address,
405                                    value.chain,
406                                    account.slots.clone(),
407                                    Some(account.native_balance),
408                                    None,
409                                    ChangeType::Update,
410                                );
411                                (*impl_addr, proxy_state)
412                            }
413                            None => {
414                                // Token does not have a proxy contract yet, create one
415
416                                // Assign original token contract to new address
417                                let impl_addr = generate_proxy_token_address(
418                                    state_guard.proxy_token_addresses.len() as u32,
419                                )?;
420                                state_guard
421                                    .proxy_token_addresses
422                                    .insert(original_address, impl_addr);
423
424                                // Add proxy token contract at original token address
425                                let proxy_state = create_proxy_token_account(
426                                    original_address,
427                                    Some(impl_addr),
428                                    &account.slots,
429                                    value.chain,
430                                    Some(account.native_balance),
431                                );
432
433                                (impl_addr, proxy_state)
434                            }
435                        };
436
437                        proxy_token_accounts.insert(original_address, proxy_state);
438
439                        // Assign original token contract to the implementation address
440                        let impl_update = ResponseAccount {
441                            address: impl_addr,
442                            slots: HashMap::new(),
443                            ..account.clone()
444                        };
445                        storage_by_address.insert(impl_addr, impl_update);
446                    } else {
447                        // Not a token, apply snapshot to the account at its original address
448                        storage_by_address.insert(account.address, account);
449                    }
450                }
451
452                // Split proxy accounts by change type:
453                // - Creation: new proxies that must overwrite any existing placeholder
454                // - Update: existing proxies whose storage is being refreshed (handled normally)
455                let mut proxy_creates: Vec<AccountUpdate> = Vec::new();
456                let mut proxy_updates: HashMap<Address, AccountUpdate> = HashMap::new();
457                for (addr, update) in proxy_token_accounts {
458                    if matches!(update.change, ChangeType::Creation) {
459                        proxy_creates.push(update);
460                    } else {
461                        proxy_updates.insert(addr, update);
462                    }
463                }
464
465                info!("Updating engine with {} contracts from snapshots", storage_by_address.len());
466                update_engine(
467                    SHARED_TYCHO_DB.clone(),
468                    header.clone().block(),
469                    Some(storage_by_address),
470                    proxy_updates,
471                )
472                .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
473
474                // Force-overwrite new proxy token accounts so that authoritative vm_storage data
475                // always wins over any empty placeholder previously inserted by engine setup
476                // (which uses init_account / init-if-not-exists).
477                if !proxy_creates.is_empty() {
478                    SHARED_TYCHO_DB
479                        .force_update_accounts(proxy_creates)
480                        .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
481                }
482                info!("Engine updated");
483                drop(state_guard);
484            }
485
486            // Construct a contract to token balances map: HashMap<ContractAddress,
487            // HashMap<TokenAddress, Balance>>
488            let account_balances = protocol_msg
489                .clone()
490                .snapshots
491                .get_vm_storage()
492                .iter()
493                .filter_map(|(addr, acc)| {
494                    if acc.token_balances.is_empty() {
495                        return None;
496                    }
497                    let balances = acc
498                        .token_balances
499                        .iter()
500                        .map(|(token_addr, ab)| (token_addr.clone(), ab.balance.clone()))
501                        .collect::<HashMap<Bytes, Bytes>>();
502                    Some((addr.clone(), balances))
503                })
504                .collect::<AccountBalances>();
505
506            let mut new_components = HashMap::new();
507            let mut count_token_skips = 0;
508            let mut components_to_store = HashMap::new();
509            {
510                let state_guard = self.state.read().await;
511
512                // PROCESS SNAPSHOTS
513                'snapshot_loop: for (id, snapshot) in protocol_msg
514                    .snapshots
515                    .get_states()
516                    .clone()
517                {
518                    // Skip any unsupported pools
519                    if self
520                        .inclusion_filters
521                        .get(protocol.as_str())
522                        .is_some_and(|predicate| !predicate(&snapshot))
523                    {
524                        continue;
525                    }
526
527                    // Construct component from snapshot
528                    let mut component_tokens = Vec::new();
529                    let mut new_tokens_accounts = HashMap::new();
530                    for token in snapshot.component.tokens.clone() {
531                        match state_guard.tokens.get(&token) {
532                            Some(token) => {
533                                component_tokens.push(token.clone());
534
535                                // If the token is not an existing proxy token, we need to add it to
536                                // the simulation engine
537                                let token_address = match bytes_to_address(&token.address) {
538                                    Ok(addr) => addr,
539                                    Err(_) => {
540                                        count_token_skips += 1;
541                                        msg_failed_components.insert(id.clone());
542                                        warn!(
543                                            "Token address could not be decoded {}, ignoring pool {:x?}",
544                                            token.address, id
545                                        );
546                                        continue 'snapshot_loop;
547                                    }
548                                };
549                                // Deploy a proxy account without an implementation set
550                                if !state_guard
551                                    .proxy_token_addresses
552                                    .contains_key(&token_address)
553                                {
554                                    new_tokens_accounts.insert(
555                                        token_address,
556                                        create_proxy_token_account(
557                                            token_address,
558                                            None,
559                                            &HashMap::new(),
560                                            snapshot.component.chain,
561                                            None,
562                                        ),
563                                    );
564                                }
565                            }
566                            None => {
567                                count_token_skips += 1;
568                                msg_failed_components.insert(id.clone());
569                                debug!("Token not found {}, ignoring pool {:x?}", token, id);
570                                continue 'snapshot_loop;
571                            }
572                        }
573                    }
574                    let component = ProtocolComponent::from_with_tokens(
575                        snapshot.component.clone(),
576                        component_tokens,
577                    );
578
579                    // Add new tokens to the simulation engine
580                    if !new_tokens_accounts.is_empty() {
581                        update_engine(
582                            SHARED_TYCHO_DB.clone(),
583                            header.clone().block(),
584                            None,
585                            new_tokens_accounts,
586                        )
587                        .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
588                    }
589
590                    // collect contracts:ids mapping for states that should update on contract
591                    // changes (non-manual updates)
592                    if !component
593                        .static_attributes
594                        .contains_key("manual_updates")
595                    {
596                        for contract in &component.contract_ids {
597                            contracts_map
598                                .entry(contract.clone())
599                                .or_insert_with(HashSet::new)
600                                .insert(id.clone());
601                        }
602                        // Add DCI contracts so changes to these contracts trigger
603                        // an update
604                        for (_, tracing) in snapshot.entrypoints.iter() {
605                            for contract in tracing.accessed_slots.keys().cloned() {
606                                contracts_map
607                                    .entry(contract)
608                                    .or_insert_with(HashSet::new)
609                                    .insert(id.clone());
610                            }
611                        }
612                    }
613
614                    // Collect new pairs (components)
615                    new_pairs.insert(id.clone(), component.clone());
616
617                    // Store component for later batch insertion
618                    components_to_store.insert(id.clone(), component);
619
620                    // Construct state from snapshot
621                    if let Some(state_decode_f) = self.registry.get(protocol.as_str()) {
622                        let live_override = self
623                            .override_providers
624                            .get(protocol.as_str())
625                            .and_then(|provider| provider.subscribe(protocol.as_str()));
626                        match state_decode_f(
627                            snapshot,
628                            header.clone(),
629                            account_balances.clone(),
630                            self.state.clone(),
631                            live_override,
632                        )
633                        .await
634                        {
635                            Ok(state) => {
636                                new_components.insert(id.clone(), state);
637                            }
638                            Err(e) => {
639                                if self.skip_state_decode_failures {
640                                    warn!(pool = id, error = %e, "StateDecodingFailure");
641                                    msg_failed_components.insert(id.clone());
642                                    continue 'snapshot_loop;
643                                } else {
644                                    error!(pool = id, error = %e, "StateDecodingFailure");
645                                    return Err(StreamDecodeError::Fatal(format!("{e}")));
646                                }
647                            }
648                        }
649                    } else if self.skip_state_decode_failures {
650                        warn!(pool = id, "MissingDecoderRegistration");
651                        msg_failed_components.insert(id.clone());
652                        continue 'snapshot_loop;
653                    } else {
654                        error!(pool = id, "MissingDecoderRegistration");
655                        return Err(StreamDecodeError::Fatal(format!(
656                            "Missing decoder registration for: {id}"
657                        )));
658                    }
659                }
660            }
661
662            // Batch insert components into state
663            if !components_to_store.is_empty() {
664                let mut state_guard = self.state.write().await;
665                for (id, component) in components_to_store {
666                    state_guard
667                        .components
668                        .insert(id, component);
669                }
670            }
671
672            if !protocol_msg.snapshots.states.is_empty() {
673                info!("Decoded {} snapshots for protocol {protocol}", new_components.len());
674            }
675            if count_token_skips > 0 {
676                info!("Skipped {count_token_skips} pools due to missing tokens");
677            }
678
679            //TODO: should we remove failed components for new_components?
680            updated_states.extend(new_components);
681
682            // PROCESS DELTAS
683            if let Some(deltas) = protocol_msg.deltas.clone() {
684                // Update engine with account changes
685                let mut state_guard = self.state.write().await;
686
687                let mut account_update_by_address: HashMap<Address, AccountUpdate> = HashMap::new();
688                // New proxy token accounts that must overwrite any existing placeholder.
689                let mut new_proxy_accounts: Vec<AccountUpdate> = Vec::new();
690                for (key, value) in deltas.account_deltas.iter() {
691                    let mut update: AccountUpdate = value.clone().into();
692
693                    // TEMP PATCH (ENG-4993)
694                    //
695                    // The indexer may emit Creation deltas with no code for EOA addresses.
696                    // Treat them as EOAs (empty code) rather than downgrading to Update, which
697                    // would skip init_account and cause "uninitialized account" warnings.
698                    if update.code.is_none() && matches!(update.change, ChangeType::Creation) {
699                        error!(
700                            update = ?update,
701                            "FaultyCreationDelta"
702                        );
703                        update.code = Some(vec![]);
704                    }
705
706                    if state_guard.tokens.contains_key(key) {
707                        let original_address = update.address;
708                        // If the account is a token, we need to handle it with a proxy contract.
709                        // Storage updates apply to the proxy contract (at original address).
710                        // Code updates (if any) apply to the token implementation contract (at
711                        // impl_addr).
712
713                        // Handle proxy contract updates
714                        let impl_addr = match state_guard
715                            .proxy_token_addresses
716                            .get(&original_address)
717                        {
718                            Some(impl_addr) => {
719                                // Token already has a proxy contract.
720
721                                // The proxy account already exists, so this is always a plain
722                                // storage update regardless of the incoming change type.
723                                let proxy_update = AccountUpdate {
724                                    code: None,
725                                    change: ChangeType::Update,
726                                    ..update.clone()
727                                };
728                                account_update_by_address.insert(original_address, proxy_update);
729
730                                *impl_addr
731                            }
732                            None => {
733                                // Token does not have a proxy contract yet, create one
734
735                                // Assign original token (implementation) contract to new proxy
736                                // address
737                                let impl_addr = generate_proxy_token_address(
738                                    state_guard.proxy_token_addresses.len() as u32,
739                                )?;
740                                state_guard
741                                    .proxy_token_addresses
742                                    .insert(original_address, impl_addr);
743
744                                // Create proxy token account with original account's storage (at
745                                // original address). Track it separately so it can be
746                                // force-overwritten and win over any placeholder that an engine
747                                // setup routine may have written earlier.
748                                let proxy_state = create_proxy_token_account(
749                                    original_address,
750                                    Some(impl_addr),
751                                    &update.slots,
752                                    update.chain,
753                                    update.balance,
754                                );
755                                new_proxy_accounts.push(proxy_state);
756
757                                impl_addr
758                            }
759                        };
760
761                        // Apply code update to token implementation contract
762                        if update.code.is_some() {
763                            let impl_update = AccountUpdate {
764                                address: impl_addr,
765                                slots: HashMap::new(),
766                                ..update.clone()
767                            };
768                            account_update_by_address.insert(impl_addr, impl_update);
769                        }
770                    } else {
771                        // Not a token, apply update to the account at its original address
772                        account_update_by_address.insert(update.address, update);
773                    }
774                }
775                drop(state_guard);
776
777                let state_guard = self.state.read().await;
778                info!("Updating engine with {} contract deltas", deltas.account_deltas.len());
779                update_engine(
780                    SHARED_TYCHO_DB.clone(),
781                    header.clone().block(),
782                    None,
783                    account_update_by_address,
784                )
785                .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
786
787                // Force-overwrite any newly-created proxy token accounts so they always win
788                // over placeholder entries inserted by engine setup.
789                if !new_proxy_accounts.is_empty() {
790                    SHARED_TYCHO_DB
791                        .force_update_accounts(new_proxy_accounts)
792                        .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
793                }
794                info!("Engine updated");
795
796                // Collect all pools related to the updated accounts
797                let mut pools_to_update = HashSet::new();
798                for (account, _update) in deltas.account_deltas {
799                    // get new pools related to the account updated
800                    pools_to_update.extend(
801                        contracts_map
802                            .get(&account)
803                            .cloned()
804                            .unwrap_or_default(),
805                    );
806                    // get existing pools related to the account updated
807                    pools_to_update.extend(
808                        state_guard
809                            .contracts_map
810                            .get(&account)
811                            .cloned()
812                            .unwrap_or_default(),
813                    );
814                }
815
816                // Collect all balance changes this block
817                let all_balances = Balances {
818                    component_balances: deltas
819                        .component_balances
820                        .iter()
821                        .map(|(pool_id, bals)| {
822                            let mut balances = HashMap::new();
823                            for (t, b) in bals {
824                                balances.insert(t.clone(), b.balance.clone());
825                            }
826                            pools_to_update.insert(pool_id.clone());
827                            (pool_id.clone(), balances)
828                        })
829                        .collect(),
830                    account_balances: deltas
831                        .account_balances
832                        .iter()
833                        .map(|(account, bals)| {
834                            let mut balances = HashMap::new();
835                            for (t, b) in bals {
836                                balances.insert(t.clone(), b.balance.clone());
837                            }
838                            pools_to_update.extend(
839                                contracts_map
840                                    .get(account)
841                                    .cloned()
842                                    .unwrap_or_default(),
843                            );
844                            (account.clone(), balances)
845                        })
846                        .collect(),
847                };
848
849                // update states with protocol state deltas (attribute changes etc.)
850                for (id, update) in deltas.state_deltas {
851                    // TODO: is this needed?
852                    let update_with_block = Self::add_block_info_to_delta(
853                        ProtocolStateDelta::from(update),
854                        current_block.clone(),
855                    );
856                    match Self::apply_update(
857                        &id,
858                        update_with_block,
859                        &mut updated_states,
860                        &state_guard,
861                        &all_balances,
862                    ) {
863                        Ok(_) => {
864                            pools_to_update.remove(&id);
865                        }
866                        Err(e) => {
867                            if self.skip_state_decode_failures {
868                                warn!(pool = id, error = %e, "Failed to apply state update, marking component as removed");
869                                // Remove from updated_states if it was there
870                                updated_states.remove(&id);
871                                // Try to get component from new_pairs first, then from state
872                                if let Some(component) = new_pairs.remove(&id) {
873                                    removed_pairs.insert(id.clone(), component);
874                                } else if let Some(component) = state_guard.components.get(&id) {
875                                    removed_pairs.insert(id.clone(), component.clone());
876                                } else {
877                                    // Component not found in new_pairs or state, this shouldn't
878                                    // happen
879                                    warn!(pool = id, "Component not found in new_pairs or state, cannot add to removed_pairs");
880                                }
881                                pools_to_update.remove(&id);
882
883                                // Add to failed components
884                                msg_failed_components.insert(id.clone());
885                            } else {
886                                return Err(e);
887                            }
888                        }
889                    }
890                }
891
892                // update remaining pools linked to updated contracts/updated balances
893                for pool in pools_to_update {
894                    // TODO: is this needed?
895                    let default_delta_with_block = Self::add_block_info_to_delta(
896                        ProtocolStateDelta::default(),
897                        current_block.clone(),
898                    );
899                    match Self::apply_update(
900                        &pool,
901                        default_delta_with_block,
902                        &mut updated_states,
903                        &state_guard,
904                        &all_balances,
905                    ) {
906                        Ok(_) => {}
907                        Err(e) => {
908                            if self.skip_state_decode_failures {
909                                warn!(pool = pool, error = %e, "Failed to apply contract/balance update, marking component as removed");
910                                // Remove from updated_states if it was there
911                                updated_states.remove(&pool);
912                                // Try to get component from new_pairs first, then from state
913                                if let Some(component) = new_pairs.remove(&pool) {
914                                    removed_pairs.insert(pool.clone(), component);
915                                } else if let Some(component) = state_guard.components.get(&pool) {
916                                    removed_pairs.insert(pool.clone(), component.clone());
917                                } else {
918                                    // Component not found in new_pairs or state, this shouldn't
919                                    // happen
920                                    warn!(pool = pool, "Component not found in new_pairs or state, cannot add to removed_pairs");
921                                }
922
923                                // Add to failed components
924                                msg_failed_components.insert(pool.clone());
925                            } else {
926                                return Err(e);
927                            }
928                        }
929                    }
930                }
931            };
932        }
933
934        // Persist the newly added/updated states
935        let mut state_guard = self.state.write().await;
936
937        // Update failed components with any new ones
938        state_guard
939            .failed_components
940            .extend(msg_failed_components);
941
942        // Remove any failed components from Updates
943        // Perf: we could do it directly in the decoder logic to avoid some steps, but this logic is
944        // complex and this is more robust.
945        updated_states.retain(|id, _| {
946            !state_guard
947                .failed_components
948                .contains(id)
949        });
950        new_pairs.retain(|id, _| {
951            !state_guard
952                .failed_components
953                .contains(id)
954        });
955
956        state_guard
957            .states
958            .extend(updated_states.clone());
959
960        state_guard.current_block_number = block_number_or_timestamp;
961
962        // Add new components to persistent state
963        for (id, component) in new_pairs.iter() {
964            state_guard
965                .components
966                .insert(id.clone(), component.clone());
967        }
968
969        // Remove components from persistent state
970        for id in removed_pairs.keys() {
971            state_guard.components.remove(id);
972        }
973
974        for (key, values) in contracts_map {
975            state_guard
976                .contracts_map
977                .entry(key)
978                .or_insert_with(HashSet::new)
979                .extend(values);
980        }
981
982        // Send the tick with all updated states
983        Ok(Update::new(block_number_or_timestamp, updated_states, new_pairs)
984            .set_is_partial(is_partial)
985            .set_removed_pairs(removed_pairs)
986            .set_sync_states(msg.sync_states.clone()))
987    }
988
989    /// Applies pending deltas from one or more `TxDeltaIndexer`s against the current confirmed
990    /// state and returns an ephemeral `Update`.
991    ///
992    /// This is the read-only counterpart of `decode()`. It clones pool states, applies the
993    /// supplied `pending_deltas`, and returns the result — **without writing back** to
994    /// `DecoderState`. Calling this method twice with the same input produces identical results.
995    ///
996    /// Only native protocols are supported. VM protocols (extractor prefix `"vm:"`) are rejected
997    /// at registration time in
998    /// [`with_pending_indexer`](crate::evm::stream::ProtocolStreamBuilder::with_pending_indexer).
999    ///
1000    /// # Parameters
1001    /// * `pending_deltas` — map from extractor name to the `BlockAggregatedChanges` produced by the
1002    ///   corresponding `TxDeltaIndexer::generate_deltas()` call.
1003    /// * `header` — the target block header. Its `block_number_or_timestamp()` is stamped on the
1004    ///   returned [`Update`]; its `block_number` and `block_timestamp` are injected into each state
1005    ///   delta so that protocols relying on block context (e.g. aerodrome slipstreams, etherfi)
1006    ///   receive correct values.
1007    pub async fn apply_deltas_ephemeral(
1008        &self,
1009        pending_deltas: &HashMap<String, BlockAggregatedChanges>,
1010        header: H,
1011    ) -> Result<Update, StreamDecodeError> {
1012        let block_number_or_timestamp = header
1013            .clone()
1014            .block_number_or_timestamp();
1015        let current_block = header.block();
1016        let state_guard = self.state.read().await;
1017
1018        let mut updated_states: HashMap<String, Box<dyn ProtocolSim>> = HashMap::new();
1019
1020        for deltas in pending_deltas.values() {
1021            let all_balances = Balances {
1022                component_balances: deltas
1023                    .component_balances
1024                    .iter()
1025                    .map(|(pool_id, bals)| {
1026                        let balances = bals
1027                            .iter()
1028                            .map(|(t, b)| (t.clone(), b.balance.clone()))
1029                            .collect();
1030                        (pool_id.clone(), balances)
1031                    })
1032                    .collect(),
1033                account_balances: HashMap::new(),
1034            };
1035
1036            for (id, state_delta) in &deltas.state_deltas {
1037                let dto_delta = Self::add_block_info_to_delta(
1038                    ProtocolStateDelta::from(state_delta.clone()),
1039                    current_block.clone(),
1040                );
1041                if let Err(e) = Self::apply_update(
1042                    id,
1043                    dto_delta,
1044                    &mut updated_states,
1045                    &state_guard,
1046                    &all_balances,
1047                ) {
1048                    warn!(pool = id, error = %e, "EphemeralDeltaTransitionError");
1049                }
1050            }
1051        }
1052
1053        Ok(Update::new(block_number_or_timestamp, updated_states, HashMap::new()))
1054    }
1055
1056    /// Add current block information (number and timestamp) to a ProtocolStateDelta.
1057    fn add_block_info_to_delta(
1058        mut delta: ProtocolStateDelta,
1059        block_header_opt: Option<BlockHeader>,
1060    ) -> ProtocolStateDelta {
1061        if let Some(header) = block_header_opt {
1062            // Add block_number and block_timestamp attributes to ensure pool states
1063            // receive current block information during delta_transition
1064            delta.updated_attributes.insert(
1065                "block_number".to_string(),
1066                Bytes::from(header.number.to_be_bytes().to_vec()),
1067            );
1068            delta.updated_attributes.insert(
1069                "block_timestamp".to_string(),
1070                Bytes::from(header.timestamp.to_be_bytes().to_vec()),
1071            );
1072        }
1073        delta
1074    }
1075
1076    fn apply_update(
1077        id: &String,
1078        update: ProtocolStateDelta,
1079        updated_states: &mut HashMap<String, Box<dyn ProtocolSim>>,
1080        state_guard: &RwLockReadGuard<'_, DecoderState>,
1081        all_balances: &Balances,
1082    ) -> Result<(), StreamDecodeError> {
1083        match updated_states.entry(id.clone()) {
1084            Entry::Occupied(mut entry) => {
1085                // If state exists in updated_states, apply the delta to it
1086                let state: &mut Box<dyn ProtocolSim> = entry.get_mut();
1087                state
1088                    .delta_transition(update, &state_guard.tokens, all_balances)
1089                    .map_err(|e| {
1090                        error!(pool = id, error = ?e, "DeltaTransitionError");
1091                        StreamDecodeError::Fatal(format!("TransitionFailure: {e:?}"))
1092                    })?;
1093            }
1094            Entry::Vacant(_) => {
1095                match state_guard.states.get(id) {
1096                    // If state does not exist in updated_states, apply the delta to the stored
1097                    // state
1098                    Some(stored_state) => {
1099                        let mut state = stored_state.clone();
1100                        state
1101                            .delta_transition(update, &state_guard.tokens, all_balances)
1102                            .map_err(|e| {
1103                                error!(pool = id, error = ?e, "DeltaTransitionError");
1104                                StreamDecodeError::Fatal(format!("TransitionFailure: {e:?}"))
1105                            })?;
1106                        updated_states.insert(id.clone(), state);
1107                    }
1108                    None => debug!(pool = id, reason = "MissingState", "DeltaTransitionError"),
1109                }
1110            }
1111        }
1112        Ok(())
1113    }
1114}
1115
1116/// Generate a proxy token address for a given token index
1117fn generate_proxy_token_address(idx: u32) -> Result<Address, StreamDecodeError> {
1118    let padded_idx = format!("{idx:x}");
1119    let padded_zeroes = "0".repeat(33 - padded_idx.len());
1120    let proxy_token_address = format!("{padded_zeroes}{padded_idx}BAdbaBe");
1121    let decoded = hex::decode(proxy_token_address).map_err(|e| {
1122        StreamDecodeError::Fatal(format!("Invalid proxy token address encoding: {e}"))
1123    })?;
1124
1125    const ADDRESS_LENGTH: usize = 20;
1126    if decoded.len() != ADDRESS_LENGTH {
1127        return Err(StreamDecodeError::Fatal(format!(
1128            "Invalid proxy token address length: expected {}, got {}",
1129            ADDRESS_LENGTH,
1130            decoded.len(),
1131        )));
1132    }
1133
1134    Ok(Address::from_slice(&decoded))
1135}
1136
1137/// Create a proxy token account for a token at a given address
1138///
1139/// The proxy token account is created at the original token address and points to the new token
1140/// address.
1141fn create_proxy_token_account(
1142    addr: Address,
1143    new_address: Option<Address>,
1144    storage: &HashMap<U256, U256>,
1145    chain: Chain,
1146    balance: Option<U256>,
1147) -> AccountUpdate {
1148    let mut slots = storage.clone();
1149    if let Some(new_address) = new_address {
1150        slots.insert(*IMPLEMENTATION_SLOT, U256::from_be_slice(new_address.as_slice()));
1151    }
1152
1153    AccountUpdate {
1154        address: addr,
1155        chain,
1156        slots,
1157        balance,
1158        code: Some(ERC20_PROXY_BYTECODE.to_vec()),
1159        change: ChangeType::Creation,
1160    }
1161}
1162
1163#[cfg(test)]
1164mock! {
1165    #[derive(Debug)]
1166    pub ProtocolSim {
1167        pub fn fee(&self) -> f64;
1168        pub fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
1169        pub fn get_amount_out(
1170            &self,
1171            amount_in: BigUint,
1172            token_in: &Token,
1173            token_out: &Token,
1174        ) -> Result<GetAmountOutResult, SimulationError>;
1175        pub fn get_limits(
1176            &self,
1177            sell_token: Bytes,
1178            buy_token: Bytes,
1179        ) -> Result<(BigUint, BigUint), SimulationError>;
1180        pub fn delta_transition(
1181            &mut self,
1182            delta: ProtocolStateDelta,
1183            tokens: &HashMap<Bytes, Token>,
1184            balances: &Balances,
1185        ) -> Result<(), TransitionError>;
1186        pub fn clone_box(&self) -> Box<dyn ProtocolSim>;
1187        pub fn eq(&self, other: &dyn ProtocolSim) -> bool;
1188    }
1189}
1190
1191#[cfg(test)]
1192crate::impl_non_serializable_protocol!(MockProtocolSim, "test protocol");
1193
1194#[cfg(test)]
1195impl ProtocolSim for MockProtocolSim {
1196    fn fee(&self) -> f64 {
1197        self.fee()
1198    }
1199
1200    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
1201        self.spot_price(base, quote)
1202    }
1203
1204    fn get_amount_out(
1205        &self,
1206        amount_in: BigUint,
1207        token_in: &Token,
1208        token_out: &Token,
1209    ) -> Result<GetAmountOutResult, SimulationError> {
1210        self.get_amount_out(amount_in, token_in, token_out)
1211    }
1212
1213    fn get_limits(
1214        &self,
1215        sell_token: Bytes,
1216        buy_token: Bytes,
1217    ) -> Result<(BigUint, BigUint), SimulationError> {
1218        self.get_limits(sell_token, buy_token)
1219    }
1220
1221    fn delta_transition(
1222        &mut self,
1223        delta: ProtocolStateDelta,
1224        tokens: &HashMap<Bytes, Token>,
1225        balances: &Balances,
1226    ) -> Result<(), TransitionError> {
1227        self.delta_transition(delta, tokens, balances)
1228    }
1229
1230    fn clone_box(&self) -> Box<dyn ProtocolSim> {
1231        self.clone_box()
1232    }
1233
1234    fn as_any(&self) -> &dyn Any {
1235        panic!("MockProtocolSim does not support as_any")
1236    }
1237
1238    fn as_any_mut(&mut self) -> &mut dyn Any {
1239        panic!("MockProtocolSim does not support as_any_mut")
1240    }
1241
1242    fn eq(&self, other: &dyn ProtocolSim) -> bool {
1243        self.eq(other)
1244    }
1245
1246    fn typetag_name(&self) -> &'static str {
1247        unreachable!()
1248    }
1249
1250    fn typetag_deserialize(&self) {
1251        unreachable!()
1252    }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257    use std::str::FromStr;
1258
1259    use alloy::primitives::address;
1260    use mockall::predicate::*;
1261    use rstest::*;
1262    use tycho_client::feed::BlockHeader;
1263    use tycho_common::{models::Chain, Bytes};
1264
1265    use super::*;
1266    use crate::evm::protocol::{curve::CurveState, uniswap_v2::state::UniswapV2State};
1267
1268    #[test]
1269    fn curve_vm_adapter_registration_flagged_deprecated() {
1270        // The native decoder is the supported path — not flagged.
1271        assert!(!is_deprecated_curve_registration::<CurveState>("vm:curve"));
1272        // Any other type for vm:curve is the deprecated VM-adapter path.
1273        assert!(is_deprecated_curve_registration::<UniswapV2State>("vm:curve"));
1274        // Other exchanges are unaffected.
1275        assert!(!is_deprecated_curve_registration::<UniswapV2State>("uniswap_v2"));
1276    }
1277
1278    async fn setup_decoder(set_tokens: bool) -> TychoStreamDecoder<BlockHeader> {
1279        let mut decoder = TychoStreamDecoder::new();
1280        decoder.register_decoder::<UniswapV2State>("uniswap_v2");
1281        if set_tokens {
1282            let tokens = [
1283                Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").lpad(20, 0),
1284                Bytes::from("0xdac17f958d2ee523a2206206994597c13d831ec7").lpad(20, 0),
1285            ]
1286            .iter()
1287            .map(|addr| {
1288                let addr_str = format!("{addr:x}");
1289                (
1290                    addr.clone(),
1291                    Token::new(addr, &addr_str, 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1292                )
1293            })
1294            .collect();
1295            decoder.set_tokens(tokens).await;
1296        }
1297        decoder
1298    }
1299
1300    fn load_test_msg(name: &str) -> FeedMessage<BlockHeader> {
1301        use std::{fs, path::Path};
1302
1303        use tycho_client::feed::dto;
1304        let project_root = env!("CARGO_MANIFEST_DIR");
1305        let asset_path = Path::new(project_root).join(format!("tests/assets/decoder/{name}.json"));
1306        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1307        let feed_msg: dto::FeedMessage<BlockHeader> =
1308            serde_json::from_str(&json_data).expect("Failed to deserialize FeedMsg json!");
1309        FeedMessage::from(feed_msg)
1310    }
1311
1312    #[tokio::test]
1313    async fn test_decode() {
1314        let decoder = setup_decoder(true).await;
1315
1316        let msg = load_test_msg("uniswap_v2_snapshot");
1317        let res1 = decoder
1318            .decode(&msg)
1319            .await
1320            .expect("decode failure");
1321        let msg = load_test_msg("uniswap_v2_delta");
1322        let res2 = decoder
1323            .decode(&msg)
1324            .await
1325            .expect("decode failure");
1326
1327        assert_eq!(res1.states.len(), 1);
1328        assert_eq!(res2.states.len(), 1);
1329        assert_eq!(res1.sync_states.len(), 1);
1330        assert_eq!(res2.sync_states.len(), 1);
1331    }
1332
1333    #[tokio::test]
1334    async fn test_decode_token_creation_delta_with_existing_proxy() {
1335        let decoder = setup_decoder(true).await;
1336        let msg = load_test_msg("uniswap_v2_delta_token_creation");
1337
1338        // First decode: the token has no proxy yet, so the Creation delta takes the
1339        // proxy-creating branch.
1340        decoder
1341            .decode(&msg)
1342            .await
1343            .expect("first decode (proxy creation) failed");
1344
1345        // Second decode: the proxy exists, so the same Creation delta must decode as a
1346        // storage update on the proxy account — not a code-less Creation, which the
1347        // engine rejects as "MissingCode".
1348        decoder
1349            .decode(&msg)
1350            .await
1351            .expect("decode of a token Creation delta with an existing proxy failed");
1352    }
1353
1354    #[tokio::test]
1355    async fn test_decode_component_missing_token() {
1356        let decoder = setup_decoder(false).await;
1357        let tokens = [Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").lpad(20, 0)]
1358            .iter()
1359            .map(|addr| {
1360                let addr_str = format!("{addr:x}");
1361                (
1362                    addr.clone(),
1363                    Token::new(addr, &addr_str, 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1364                )
1365            })
1366            .collect();
1367        decoder.set_tokens(tokens).await;
1368
1369        let msg = load_test_msg("uniswap_v2_snapshot");
1370        let res1 = decoder
1371            .decode(&msg)
1372            .await
1373            .expect("decode failure");
1374
1375        assert_eq!(res1.states.len(), 0);
1376    }
1377
1378    #[tokio::test]
1379    async fn test_decode_component_bad_id() {
1380        let decoder = setup_decoder(true).await;
1381        let msg = load_test_msg("uniswap_v2_snapshot_broken_id");
1382
1383        match decoder.decode(&msg).await {
1384            Err(StreamDecodeError::Fatal(msg)) => {
1385                assert_eq!(msg, "Component id mismatch");
1386            }
1387            Ok(_) => {
1388                panic!("Expected failures to be raised")
1389            }
1390        }
1391    }
1392
1393    #[rstest]
1394    #[case(true)]
1395    #[case(false)]
1396    #[tokio::test]
1397    async fn test_decode_component_bad_state(#[case] skip_failures: bool) {
1398        let mut decoder = setup_decoder(true).await;
1399        decoder.skip_state_decode_failures = skip_failures;
1400
1401        let msg = load_test_msg("uniswap_v2_snapshot_broken_state");
1402        match decoder.decode(&msg).await {
1403            Err(StreamDecodeError::Fatal(msg)) => {
1404                if !skip_failures {
1405                    assert_eq!(msg, "Missing attributes reserve0");
1406                } else {
1407                    panic!("Expected failures to be ignored. Err: {msg}")
1408                }
1409            }
1410            Ok(res) => {
1411                if !skip_failures {
1412                    panic!("Expected failures to be raised")
1413                } else {
1414                    assert_eq!(res.states.len(), 0);
1415                }
1416            }
1417        }
1418    }
1419
1420    #[tokio::test]
1421    async fn test_decode_updates_state_on_contract_change() {
1422        let decoder = setup_decoder(true).await;
1423
1424        // Create the mock instances
1425        let mut mock_state = MockProtocolSim::new();
1426
1427        mock_state
1428            .expect_clone_box()
1429            .times(1)
1430            .returning(|| {
1431                let mut cloned_mock_state = MockProtocolSim::new();
1432                // Expect `delta_transition` to be called once with any parameters
1433                cloned_mock_state
1434                    .expect_delta_transition()
1435                    .times(1)
1436                    .returning(|_, _, _| Ok(()));
1437                cloned_mock_state
1438                    .expect_clone_box()
1439                    .times(1)
1440                    .returning(|| Box::new(MockProtocolSim::new()));
1441                Box::new(cloned_mock_state)
1442            });
1443
1444        // Insert mock state into `updated_states`
1445        let pool_id =
1446            "0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2".to_string();
1447        decoder
1448            .state
1449            .write()
1450            .await
1451            .states
1452            .insert(pool_id.clone(), Box::new(mock_state) as Box<dyn ProtocolSim>);
1453        decoder
1454            .state
1455            .write()
1456            .await
1457            .contracts_map
1458            .insert(
1459                Bytes::from("0xba12222222228d8ba445958a75a0704d566bf2c8").lpad(20, 0),
1460                HashSet::from([pool_id.clone()]),
1461            );
1462
1463        // Load a test message containing a contract update
1464        let msg = load_test_msg("balancer_v2_delta");
1465
1466        // Decode the message
1467        let _ = decoder
1468            .decode(&msg)
1469            .await
1470            .expect("decode failure");
1471
1472        // The mock framework will assert that `delta_transition` was called exactly once
1473    }
1474
1475    #[test]
1476    fn test_generate_proxy_token_address() {
1477        let idx = 1;
1478        let generated_address =
1479            generate_proxy_token_address(idx).expect("proxy token address should be valid");
1480        assert_eq!(generated_address, address!("000000000000000000000000000000001badbabe"));
1481
1482        let idx = 123456;
1483        let generated_address =
1484            generate_proxy_token_address(idx).expect("proxy token address should be valid");
1485        assert_eq!(generated_address, address!("00000000000000000000000000001e240badbabe"));
1486    }
1487
1488    #[tokio::test(flavor = "multi_thread")]
1489    async fn test_euler_hook_low_pool_manager_balance() {
1490        let mut decoder = TychoStreamDecoder::new();
1491
1492        decoder.register_decoder_with_context::<crate::evm::protocol::uniswap_v4::state::UniswapV4State>(
1493            "uniswap_v4_hooks", DecoderContext::new().vm_traces(true)
1494        );
1495
1496        let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
1497        let teth = Bytes::from_str("0xd11c452fc99cf405034ee446803b6f6c1f6d5ed8").unwrap();
1498        let tokens = HashMap::from([
1499            (
1500                weth.clone(),
1501                Token::new(&weth, "WETH", 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1502            ),
1503            (
1504                teth.clone(),
1505                Token::new(&teth, "tETH", 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1506            ),
1507        ]);
1508
1509        decoder.set_tokens(tokens.clone()).await;
1510
1511        let msg = load_test_msg("euler_hook_snapshot");
1512        let res = decoder
1513            .decode(&msg)
1514            .await
1515            .expect("decode failure");
1516
1517        let pool_state = res
1518            .states
1519            .get("0xc70d7fbd7fcccdf726e02fed78548b40dc52502b097c7a1ee7d995f4d4396134")
1520            .expect("Couldn't find target pool");
1521        let amount_out = pool_state
1522            .get_amount_out(
1523                BigUint::from_str("1000000000000000000").unwrap(),
1524                tokens.get(&teth).unwrap(),
1525                tokens.get(&weth).unwrap(),
1526            )
1527            .expect("Get amount out failed");
1528
1529        assert_eq!(amount_out.amount, BigUint::from_str("1216190190361759119").unwrap());
1530    }
1531}