Skip to main content

nautilus_data/defi/
engine.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! DeFi-specific data engine functionality.
17//!
18//! This module provides DeFi processing methods for the `DataEngine`.
19//! All code in this module requires the `defi` feature flag.
20
21use std::{rc::Rc, sync::Arc};
22
23use nautilus_common::{
24    defi,
25    messages::defi::{
26        DefiRequestCommand, DefiSubscribeCommand, DefiUnsubscribeCommand, RequestPoolSnapshot,
27    },
28    msgbus::{self, TypedHandler},
29};
30use nautilus_core::UUID4;
31use nautilus_model::{
32    defi::{
33        Blockchain, DefiData, PoolProfiler,
34        data::{DexPoolData, block::BlockPosition},
35    },
36    identifiers::{ClientId, InstrumentId},
37    instruments::{CurrencyPair, InstrumentAny},
38};
39
40use crate::engine::{
41    DataEngine,
42    pool::{
43        PoolCollectHandler, PoolFlashHandler, PoolLiquidityHandler, PoolSwapHandler, PoolUpdater,
44    },
45};
46
47/// Extracts the block position tuple from a `DexPoolData` event.
48fn get_event_block_position(event: &DexPoolData) -> (u64, u32, u32) {
49    match event {
50        DexPoolData::Swap(s) => (s.block, s.transaction_index, s.log_index),
51        DexPoolData::LiquidityUpdate(u) => (u.block, u.transaction_index, u.log_index),
52        DexPoolData::FeeCollect(c) => (c.block, c.transaction_index, c.log_index),
53        DexPoolData::FeeProtocolUpdate(u) => (u.block, u.transaction_index, u.log_index),
54        DexPoolData::FeeProtocolCollect(c) => (c.block, c.transaction_index, c.log_index),
55        DexPoolData::Flash(f) => (f.block, f.transaction_index, f.log_index),
56    }
57}
58
59/// Converts buffered `DefiData` events to `DexPoolData` and sorts by block position.
60fn convert_and_sort_buffered_events(buffered_events: Vec<DefiData>) -> Vec<DexPoolData> {
61    let mut events: Vec<DexPoolData> = buffered_events
62        .into_iter()
63        .filter_map(|event| match event {
64            DefiData::PoolSwap(swap) => Some(DexPoolData::Swap(swap)),
65            DefiData::PoolLiquidityUpdate(update) => Some(DexPoolData::LiquidityUpdate(update)),
66            DefiData::PoolFeeCollect(collect) => Some(DexPoolData::FeeCollect(collect)),
67            DefiData::PoolFeeProtocolUpdate(update) => Some(DexPoolData::FeeProtocolUpdate(update)),
68            DefiData::PoolFeeProtocolCollect(collect) => {
69                Some(DexPoolData::FeeProtocolCollect(collect))
70            }
71            DefiData::PoolFlash(flash) => Some(DexPoolData::Flash(flash)),
72            _ => None,
73        })
74        .collect();
75
76    events.sort_by(|a, b| {
77        let pos_a = get_event_block_position(a);
78        let pos_b = get_event_block_position(b);
79        pos_a.cmp(&pos_b)
80    });
81
82    events
83}
84
85impl DataEngine {
86    /// Returns all blockchains for which blocks subscriptions exist.
87    #[must_use]
88    pub fn subscribed_blocks(&self) -> Vec<Blockchain> {
89        self.collect_subscriptions(|client| &client.subscriptions_blocks)
90    }
91
92    /// Returns all instrument IDs for which pool subscriptions exist.
93    #[must_use]
94    pub fn subscribed_pools(&self) -> Vec<InstrumentId> {
95        self.collect_subscriptions(|client| &client.subscriptions_pools)
96    }
97
98    /// Returns all instrument IDs for which swap subscriptions exist.
99    #[must_use]
100    pub fn subscribed_pool_swaps(&self) -> Vec<InstrumentId> {
101        self.collect_subscriptions(|client| &client.subscriptions_pool_swaps)
102    }
103
104    /// Returns all instrument IDs for which liquidity update subscriptions exist.
105    #[must_use]
106    pub fn subscribed_pool_liquidity_updates(&self) -> Vec<InstrumentId> {
107        self.collect_subscriptions(|client| &client.subscriptions_pool_liquidity_updates)
108    }
109
110    /// Returns all instrument IDs for which fee collect subscriptions exist.
111    #[must_use]
112    pub fn subscribed_pool_fee_collects(&self) -> Vec<InstrumentId> {
113        self.collect_subscriptions(|client| &client.subscriptions_pool_fee_collects)
114    }
115
116    /// Returns all instrument IDs for which flash loan subscriptions exist.
117    #[must_use]
118    pub fn subscribed_pool_flash(&self) -> Vec<InstrumentId> {
119        self.collect_subscriptions(|client| &client.subscriptions_pool_flash)
120    }
121
122    /// Handles a subscribe command, updating internal state and forwarding to the client.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the subscription is invalid (e.g., synthetic instrument for book data),
127    /// or if the underlying client operation fails.
128    pub fn execute_defi_subscribe(&mut self, cmd: DefiSubscribeCommand) -> anyhow::Result<()> {
129        if let Some(client_id) = cmd.client_id()
130            && self.external_clients.contains(client_id)
131        {
132            if self.config.debug {
133                log::debug!("Skipping defi subscribe for external client {client_id}: {cmd:?}");
134            }
135            return Ok(());
136        }
137
138        if let Some(client) = self.get_client(cmd.client_id(), cmd.venue()) {
139            log::info!("Forwarding subscription to client {}", client.client_id);
140            client.execute_defi_subscribe(cmd.clone());
141        } else {
142            log::error!(
143                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
144                cmd.client_id(),
145                cmd.venue(),
146            );
147        }
148
149        match cmd {
150            DefiSubscribeCommand::Pool(cmd) => {
151                self.setup_pool_updater(&cmd.instrument_id, cmd.client_id.as_ref());
152            }
153            DefiSubscribeCommand::PoolSwaps(cmd) => {
154                self.setup_pool_updater(&cmd.instrument_id, cmd.client_id.as_ref());
155            }
156            DefiSubscribeCommand::PoolLiquidityUpdates(cmd) => {
157                self.setup_pool_updater(&cmd.instrument_id, cmd.client_id.as_ref());
158            }
159            DefiSubscribeCommand::PoolFeeCollects(cmd) => {
160                self.setup_pool_updater(&cmd.instrument_id, cmd.client_id.as_ref());
161            }
162            DefiSubscribeCommand::PoolFlashEvents(cmd) => {
163                self.setup_pool_updater(&cmd.instrument_id, cmd.client_id.as_ref());
164            }
165            DefiSubscribeCommand::Blocks(_) => {} // No pool setup needed for blocks
166        }
167
168        Ok(())
169    }
170
171    /// Handles an unsubscribe command, updating internal state and forwarding to the client.
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if the underlying client operation fails.
176    pub fn execute_defi_unsubscribe(&mut self, cmd: &DefiUnsubscribeCommand) -> anyhow::Result<()> {
177        if let Some(client_id) = cmd.client_id()
178            && self.external_clients.contains(client_id)
179        {
180            if self.config.debug {
181                log::debug!("Skipping defi unsubscribe for external client {client_id}: {cmd:?}");
182            }
183            return Ok(());
184        }
185
186        if let Some(client) = self.get_client(cmd.client_id(), cmd.venue()) {
187            client.execute_defi_unsubscribe(cmd);
188        } else {
189            log::error!(
190                "Cannot handle command: no client found for client_id={:?}, venue={:?}",
191                cmd.client_id(),
192                cmd.venue(),
193            );
194        }
195
196        Ok(())
197    }
198
199    /// Sends a [`DefiRequestCommand`] to a suitable data client implementation.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if no client is found for the given client ID or venue,
204    /// or if the client fails to process the request.
205    pub fn execute_defi_request(&mut self, req: DefiRequestCommand) -> anyhow::Result<()> {
206        // Skip requests for external clients
207        if let Some(cid) = req.client_id()
208            && self.external_clients.contains(cid)
209        {
210            if self.config.debug {
211                log::debug!("Skipping defi data request for external client {cid}: {req:?}");
212            }
213            return Ok(());
214        }
215
216        if let Some(client) = self.get_client(req.client_id(), req.venue()) {
217            client.execute_defi_request(req)
218        } else {
219            anyhow::bail!(
220                "Cannot handle request: no client found for {:?} {:?}",
221                req.client_id(),
222                req.venue()
223            );
224        }
225    }
226
227    /// Processes DeFi-specific data events.
228    pub fn process_defi_data(&mut self, data: DefiData) {
229        self.increment_data_count();
230
231        match data {
232            DefiData::Block(block) => {
233                let topic = defi::switchboard::get_defi_blocks_topic(block.chain());
234                msgbus::publish_defi_block(topic, &block);
235            }
236            DefiData::Pool(pool) => {
237                if let Err(e) = self.cache.borrow_mut().add_pool(pool.clone()) {
238                    log::error!("Failed to add Pool to cache: {e}");
239                }
240
241                match CurrencyPair::try_from(&pool) {
242                    Ok(instrument) => {
243                        self.handle_instrument(&InstrumentAny::CurrencyPair(instrument));
244                    }
245                    Err(e) => {
246                        log::error!(
247                            "Failed to create instrument for Pool {}: {e}",
248                            pool.instrument_id
249                        );
250                    }
251                }
252
253                // Check if pool profiler creation was deferred. When a snapshot is also
254                // pending, leave the updater marker in place so a second subscription cannot
255                // race ahead and install a duplicate updater. The snapshot handler clears
256                // both flags and creates the updater once.
257                if self.pool_updaters_pending.contains(&pool.instrument_id) {
258                    if self.pool_snapshot_pending.contains(&pool.instrument_id) {
259                        log::debug!(
260                            "Pool {} loaded; deferring profiler creation to snapshot handler",
261                            pool.instrument_id
262                        );
263                    } else {
264                        self.pool_updaters_pending.remove(&pool.instrument_id);
265                        log::info!(
266                            "Pool {} now loaded, creating deferred pool profiler",
267                            pool.instrument_id
268                        );
269                        self.setup_pool_updater(&pool.instrument_id, None);
270                    }
271                }
272
273                let topic = defi::switchboard::get_defi_pool_topic(pool.instrument_id);
274                msgbus::publish_defi_pool(topic, &pool);
275            }
276            DefiData::PoolSnapshot(snapshot) => {
277                let instrument_id = snapshot.instrument_id;
278                log::info!(
279                    "Received pool snapshot for {instrument_id} at block {} with {} positions and {} ticks",
280                    snapshot.block_position.number,
281                    snapshot.positions.len(),
282                    snapshot.ticks.len()
283                );
284
285                // Validate we're expecting this snapshot
286                if !self.pool_snapshot_pending.contains(&instrument_id) {
287                    log::warn!(
288                        "Received unexpected pool snapshot for {instrument_id} (not in pending set)"
289                    );
290                    return;
291                }
292
293                // Get pool from cache
294                let pool = match self.cache.borrow().pool(&instrument_id) {
295                    Some(pool) => Arc::new(pool.clone()),
296                    None => {
297                        log::error!(
298                            "Pool {instrument_id} not found in cache when processing snapshot"
299                        );
300                        return;
301                    }
302                };
303
304                // Defensive: refuse stub snapshots that slipped past the bootstrap-side
305                // guards. Installing one would leave Python actors observing an initialized
306                // profiler with zero liquidity; better to leave the pool without a profiler
307                // so the bad state is visible.
308                if snapshot.positions.is_empty()
309                    && snapshot.ticks.is_empty()
310                    && snapshot.block_position.number == pool.creation_block
311                {
312                    log::warn!(
313                        "Refusing empty stub snapshot for {instrument_id} at pool creation block {}; pool will remain without profiler",
314                        snapshot.block_position.number,
315                    );
316                    self.pool_snapshot_pending.remove(&instrument_id);
317                    self.pool_updaters_pending.remove(&instrument_id);
318                    self.pool_event_buffers.remove(&instrument_id);
319                    return;
320                }
321
322                // Create profiler and restore from snapshot
323                let mut profiler = PoolProfiler::new(pool);
324                if let Err(e) = profiler.restore_from_snapshot(snapshot.clone()) {
325                    log::error!(
326                        "Failed to restore profiler from snapshot for {instrument_id}: {e}"
327                    );
328                    return;
329                }
330                log::debug!("Restored pool profiler for {instrument_id} from snapshot");
331
332                // Process buffered events
333                let buffered_events = self
334                    .pool_event_buffers
335                    .remove(&instrument_id)
336                    .unwrap_or_default();
337
338                if !buffered_events.is_empty() {
339                    log::info!(
340                        "Processing {} buffered events for {instrument_id}",
341                        buffered_events.len()
342                    );
343
344                    let events_to_apply = convert_and_sort_buffered_events(buffered_events);
345                    let applied_count = Self::apply_buffered_events_to_profiler(
346                        &mut profiler,
347                        events_to_apply,
348                        &snapshot.block_position,
349                        instrument_id,
350                    );
351
352                    log::info!(
353                        "Applied {applied_count} buffered events to profiler for {instrument_id}"
354                    );
355                }
356
357                // Add profiler to cache
358                if let Err(e) = self.cache.borrow_mut().add_pool_profiler(profiler) {
359                    log::error!("Failed to add pool profiler to cache for {instrument_id}: {e}");
360                    return;
361                }
362
363                // Create updater and subscribe to topics
364                self.pool_snapshot_pending.remove(&instrument_id);
365                self.pool_updaters_pending.remove(&instrument_id);
366                let updater = Rc::new(PoolUpdater::new(&instrument_id, self.cache.clone()));
367
368                self.subscribe_pool_updater_topics(instrument_id, updater.clone());
369                self.pool_updaters.insert(instrument_id, updater);
370
371                log::info!(
372                    "Pool profiler setup completed for {instrument_id}, now processing live events"
373                );
374            }
375            DefiData::PoolSwap(swap) => {
376                let instrument_id = swap.instrument_id;
377                // Buffer if waiting for snapshot, otherwise publish
378                if self.pool_snapshot_pending.contains(&instrument_id) {
379                    log::debug!("Buffering swap event for {instrument_id} (waiting for snapshot)");
380                    self.pool_event_buffers
381                        .entry(instrument_id)
382                        .or_default()
383                        .push(DefiData::PoolSwap(swap));
384                } else {
385                    let topic = defi::switchboard::get_defi_pool_swaps_topic(instrument_id);
386                    msgbus::publish_defi_swap(topic, &swap);
387                }
388            }
389            DefiData::PoolLiquidityUpdate(update) => {
390                let instrument_id = update.instrument_id;
391                // Buffer if waiting for snapshot, otherwise publish
392                if self.pool_snapshot_pending.contains(&instrument_id) {
393                    log::debug!(
394                        "Buffering liquidity update event for {instrument_id} (waiting for snapshot)"
395                    );
396                    self.pool_event_buffers
397                        .entry(instrument_id)
398                        .or_default()
399                        .push(DefiData::PoolLiquidityUpdate(update));
400                } else {
401                    let topic = defi::switchboard::get_defi_liquidity_topic(instrument_id);
402                    msgbus::publish_defi_liquidity(topic, &update);
403                }
404            }
405            DefiData::PoolFeeCollect(collect) => {
406                let instrument_id = collect.instrument_id;
407                // Buffer if waiting for snapshot, otherwise publish
408                if self.pool_snapshot_pending.contains(&instrument_id) {
409                    log::debug!(
410                        "Buffering fee collect event for {instrument_id} (waiting for snapshot)"
411                    );
412                    self.pool_event_buffers
413                        .entry(instrument_id)
414                        .or_default()
415                        .push(DefiData::PoolFeeCollect(collect));
416                } else {
417                    let topic = defi::switchboard::get_defi_collect_topic(instrument_id);
418                    msgbus::publish_defi_collect(topic, &collect);
419                }
420            }
421            DefiData::PoolFeeProtocolUpdate(update) => {
422                let instrument_id = update.instrument_id;
423                // A protocol-fee change is profiler infrastructure, not a subscriber data stream:
424                // it only needs to keep the profiler's `fee_protocol` consistent for swap and flash
425                // fee splitting. Buffer until the snapshot lands, otherwise apply in place.
426                if self.pool_snapshot_pending.contains(&instrument_id) {
427                    log::debug!(
428                        "Buffering fee protocol update for {instrument_id} (waiting for snapshot)"
429                    );
430                    self.pool_event_buffers
431                        .entry(instrument_id)
432                        .or_default()
433                        .push(DefiData::PoolFeeProtocolUpdate(update));
434                } else if let Some(profiler) =
435                    self.cache.borrow_mut().pool_profiler_mut(&instrument_id)
436                    && let Err(e) = profiler.process_fee_protocol_update(&update)
437                {
438                    log::error!("Failed to process pool fee protocol update: {e}");
439                }
440            }
441            DefiData::PoolFeeProtocolCollect(collect) => {
442                let instrument_id = collect.instrument_id;
443                // A protocol-fee withdrawal is profiler infrastructure, not a subscriber data
444                // stream: it only decrements the profiler's accrued protocol-fee balances. Buffer
445                // until the snapshot lands, otherwise apply in place.
446                if self.pool_snapshot_pending.contains(&instrument_id) {
447                    log::debug!(
448                        "Buffering fee protocol collect event for {instrument_id} (waiting for snapshot)"
449                    );
450                    self.pool_event_buffers
451                        .entry(instrument_id)
452                        .or_default()
453                        .push(DefiData::PoolFeeProtocolCollect(collect));
454                } else if let Some(profiler) =
455                    self.cache.borrow_mut().pool_profiler_mut(&instrument_id)
456                    && let Err(e) = profiler.process_fee_protocol_collect(&collect)
457                {
458                    log::error!("Failed to process pool fee protocol collect event: {e}");
459                }
460            }
461            DefiData::PoolFlash(flash) => {
462                let instrument_id = flash.instrument_id;
463                // Buffer if waiting for snapshot, otherwise publish
464                if self.pool_snapshot_pending.contains(&instrument_id) {
465                    log::debug!("Buffering flash event for {instrument_id} (waiting for snapshot)");
466                    self.pool_event_buffers
467                        .entry(instrument_id)
468                        .or_default()
469                        .push(DefiData::PoolFlash(flash));
470                } else {
471                    let topic = defi::switchboard::get_defi_flash_topic(instrument_id);
472                    msgbus::publish_defi_flash(topic, &flash);
473                }
474            }
475        }
476    }
477
478    /// Subscribes a pool updater to all relevant pool data topics using typed handlers.
479    fn subscribe_pool_updater_topics(&self, instrument_id: InstrumentId, updater: Rc<PoolUpdater>) {
480        let priority = Some(self.msgbus_priority);
481
482        // Subscribe swap handler
483        let swap_topic = defi::switchboard::get_defi_pool_swaps_topic(instrument_id);
484        let swap_handler = TypedHandler(Rc::new(PoolSwapHandler::new(updater.clone())));
485        msgbus::subscribe_defi_swaps(swap_topic.into(), swap_handler, priority);
486
487        // Subscribe liquidity handler
488        let liq_topic = defi::switchboard::get_defi_liquidity_topic(instrument_id);
489        let liq_handler = TypedHandler(Rc::new(PoolLiquidityHandler::new(updater.clone())));
490        msgbus::subscribe_defi_liquidity(liq_topic.into(), liq_handler, priority);
491
492        // Subscribe collect handler
493        let collect_topic = defi::switchboard::get_defi_collect_topic(instrument_id);
494        let collect_handler = TypedHandler(Rc::new(PoolCollectHandler::new(updater.clone())));
495        msgbus::subscribe_defi_collects(collect_topic.into(), collect_handler, priority);
496
497        // Subscribe flash handler
498        let flash_topic = defi::switchboard::get_defi_flash_topic(instrument_id);
499        let flash_handler = TypedHandler(Rc::new(PoolFlashHandler::new(updater)));
500        msgbus::subscribe_defi_flash(flash_topic.into(), flash_handler, priority);
501    }
502
503    /// Applies buffered events to a pool profiler, filtering to events after the snapshot.
504    ///
505    /// Returns the count of successfully applied events.
506    fn apply_buffered_events_to_profiler(
507        profiler: &mut PoolProfiler,
508        events: Vec<DexPoolData>,
509        snapshot_block: &BlockPosition,
510        instrument_id: InstrumentId,
511    ) -> usize {
512        let mut applied_count = 0;
513
514        for event in events {
515            let event_block = get_event_block_position(&event);
516
517            // Only apply events that occurred after the snapshot
518            let is_after_snapshot = event_block.0 > snapshot_block.number
519                || (event_block.0 == snapshot_block.number
520                    && event_block.1 > snapshot_block.transaction_index)
521                || (event_block.0 == snapshot_block.number
522                    && event_block.1 == snapshot_block.transaction_index
523                    && event_block.2 > snapshot_block.log_index);
524
525            if is_after_snapshot {
526                if let Err(e) = profiler.process(&event) {
527                    log::error!(
528                        "Failed to apply buffered event to profiler for {instrument_id}: {e}"
529                    );
530                } else {
531                    applied_count += 1;
532                }
533            }
534        }
535
536        applied_count
537    }
538
539    fn setup_pool_updater(&mut self, instrument_id: &InstrumentId, client_id: Option<&ClientId>) {
540        // Early return if updater already exists or we are in the middle of setting it up.
541        if self.pool_updaters.contains_key(instrument_id)
542            || self.pool_updaters_pending.contains(instrument_id)
543        {
544            log::debug!("Pool updater for {instrument_id} already exists");
545            return;
546        }
547
548        log::info!("Setting up pool updater for {instrument_id}");
549
550        // Check cache state and ensure profiler exists
551        {
552            let mut cache = self.cache.borrow_mut();
553
554            if cache.pool_profiler(instrument_id).is_some() {
555                // Profiler already exists, proceed to create updater
556                log::debug!("Pool profiler already exists for {instrument_id}");
557            } else if let Some(pool) = cache.pool(instrument_id) {
558                // Pool exists but no profiler, create profiler from pool
559                let pool = Arc::new(pool.clone());
560                let mut pool_profiler = PoolProfiler::new(pool.clone());
561
562                if let Some(initial_sqrt_price_x96) = pool.initial_sqrt_price_x96 {
563                    if let Err(e) = pool_profiler.initialize(initial_sqrt_price_x96) {
564                        log::error!("Failed to initialize pool profiler for {instrument_id}: {e}");
565                        drop(cache);
566                        return;
567                    }
568                    log::debug!(
569                        "Initialized pool profiler for {instrument_id} with sqrt_price {initial_sqrt_price_x96}"
570                    );
571                } else {
572                    log::debug!("Created pool profiler for {instrument_id}");
573                }
574
575                if let Err(e) = cache.add_pool_profiler(pool_profiler) {
576                    log::error!("Failed to add pool profiler for {instrument_id}: {e}");
577                    drop(cache);
578                    return;
579                }
580                drop(cache);
581            } else {
582                // Neither profiler nor pool exists, request snapshot
583                drop(cache);
584
585                let request_id = UUID4::new();
586                let ts_init = self.clock.borrow().timestamp_ns();
587                let request = RequestPoolSnapshot::new(
588                    *instrument_id,
589                    client_id.copied(),
590                    request_id,
591                    ts_init,
592                    None,
593                );
594
595                if let Err(e) = self.execute_defi_request(DefiRequestCommand::PoolSnapshot(request))
596                {
597                    log::warn!("Failed to request pool snapshot for {instrument_id}: {e}");
598                } else {
599                    log::debug!("Requested pool snapshot for {instrument_id}");
600                    self.pool_snapshot_pending.insert(*instrument_id);
601                    self.pool_updaters_pending.insert(*instrument_id);
602                    self.pool_event_buffers.entry(*instrument_id).or_default();
603                }
604                return;
605            }
606        }
607
608        // Profiler exists, create updater and subscribe to topics
609        let updater = Rc::new(PoolUpdater::new(instrument_id, self.cache.clone()));
610
611        self.subscribe_pool_updater_topics(*instrument_id, updater.clone());
612        self.pool_updaters.insert(*instrument_id, updater);
613
614        log::debug!("Created PoolUpdater for instrument ID {instrument_id}");
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use std::sync::Arc;
621
622    use alloy_primitives::{Address, I256, U160, U256};
623    use nautilus_core::UnixNanos;
624    use nautilus_model::{
625        defi::{
626            Chain, DefiData, PoolFeeCollect, PoolFeeProtocolUpdate, PoolFlash, PoolIdentifier,
627            PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap,
628            chain::chains,
629            data::DexPoolData,
630            dex::{AmmType, Dex, DexType},
631        },
632        identifiers::{InstrumentId, Symbol, Venue},
633    };
634    use rstest::*;
635
636    use super::*;
637
638    #[fixture]
639    fn test_instrument_id() -> InstrumentId {
640        InstrumentId::new(Symbol::from("ETH/USDC"), Venue::from("UNISWAPV3"))
641    }
642
643    #[fixture]
644    fn test_chain() -> Arc<Chain> {
645        Arc::new(chains::ETHEREUM.clone())
646    }
647
648    #[fixture]
649    fn test_dex(test_chain: Arc<Chain>) -> Arc<Dex> {
650        Arc::new(Dex::new(
651            (*test_chain).clone(),
652            DexType::UniswapV3,
653            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
654            12369621,
655            AmmType::CLAMM,
656            "PoolCreated(address,address,uint24,int24,address)",
657            "Swap(address,address,int256,int256,uint160,uint128,int24)",
658            "Mint(address,address,int24,int24,uint128,uint256,uint256)",
659            "Burn(address,int24,int24,uint128,uint256,uint256)",
660            "Collect(address,address,int24,int24,uint128,uint128)",
661        ))
662    }
663
664    fn create_test_swap(
665        test_instrument_id: InstrumentId,
666        test_chain: Arc<Chain>,
667        test_dex: Arc<Dex>,
668        block: u64,
669        tx_index: u32,
670        log_index: u32,
671    ) -> PoolSwap {
672        PoolSwap::new(
673            test_chain,
674            test_dex,
675            test_instrument_id,
676            PoolIdentifier::from_address(Address::ZERO),
677            block,
678            format!("0x{block:064x}"),
679            tx_index,
680            log_index,
681            UnixNanos::default(),
682            UnixNanos::default(),
683            Address::ZERO,
684            Address::ZERO,
685            I256::ZERO,
686            I256::ZERO,
687            U160::ZERO,
688            0,
689            0,
690        )
691    }
692
693    fn create_test_liquidity_update(
694        test_instrument_id: InstrumentId,
695        test_chain: Arc<Chain>,
696        test_dex: Arc<Dex>,
697        block: u64,
698        tx_index: u32,
699        log_index: u32,
700    ) -> PoolLiquidityUpdate {
701        PoolLiquidityUpdate::new(
702            test_chain,
703            test_dex,
704            test_instrument_id,
705            PoolIdentifier::from_address(Address::ZERO),
706            PoolLiquidityUpdateType::Mint,
707            block,
708            format!("0x{block:064x}"),
709            tx_index,
710            log_index,
711            None,
712            Address::ZERO,
713            0,
714            U256::ZERO,
715            U256::ZERO,
716            0,
717            0,
718            UnixNanos::default(),
719            UnixNanos::default(),
720        )
721    }
722
723    fn create_test_fee_collect(
724        test_instrument_id: InstrumentId,
725        test_chain: Arc<Chain>,
726        test_dex: Arc<Dex>,
727        block: u64,
728        tx_index: u32,
729        log_index: u32,
730    ) -> PoolFeeCollect {
731        PoolFeeCollect::new(
732            test_chain,
733            test_dex,
734            test_instrument_id,
735            PoolIdentifier::from_address(Address::ZERO),
736            block,
737            format!("0x{block:064x}"),
738            tx_index,
739            log_index,
740            Address::ZERO,
741            0,
742            0,
743            0,
744            0,
745            UnixNanos::default(),
746            UnixNanos::default(),
747        )
748    }
749
750    fn create_test_flash(
751        test_instrument_id: InstrumentId,
752        test_chain: Arc<Chain>,
753        test_dex: Arc<Dex>,
754        block: u64,
755        tx_index: u32,
756        log_index: u32,
757    ) -> PoolFlash {
758        PoolFlash::new(
759            test_chain,
760            test_dex,
761            test_instrument_id,
762            PoolIdentifier::from_address(Address::ZERO),
763            block,
764            format!("0x{block:064x}"),
765            tx_index,
766            log_index,
767            UnixNanos::default(),
768            UnixNanos::default(),
769            Address::ZERO,
770            Address::ZERO,
771            U256::ZERO,
772            U256::ZERO,
773            U256::ZERO,
774            U256::ZERO,
775        )
776    }
777
778    fn create_test_fee_protocol_update(
779        test_instrument_id: InstrumentId,
780        test_chain: Arc<Chain>,
781        test_dex: Arc<Dex>,
782        block: u64,
783        tx_index: u32,
784        log_index: u32,
785    ) -> PoolFeeProtocolUpdate {
786        PoolFeeProtocolUpdate::new(
787            test_chain,
788            test_dex,
789            test_instrument_id,
790            PoolIdentifier::from_address(Address::ZERO),
791            block,
792            format!("0x{block:064x}"),
793            tx_index,
794            log_index,
795            4,
796            4,
797            UnixNanos::default(),
798            UnixNanos::default(),
799        )
800    }
801
802    #[rstest]
803    fn test_get_event_block_position_swap(
804        test_instrument_id: InstrumentId,
805        test_chain: Arc<Chain>,
806        test_dex: Arc<Dex>,
807    ) {
808        let swap = create_test_swap(test_instrument_id, test_chain, test_dex, 100, 5, 3);
809        let pos = get_event_block_position(&DexPoolData::Swap(swap));
810        assert_eq!(pos, (100, 5, 3));
811    }
812
813    #[rstest]
814    fn test_get_event_block_position_liquidity_update(
815        test_instrument_id: InstrumentId,
816        test_chain: Arc<Chain>,
817        test_dex: Arc<Dex>,
818    ) {
819        let update =
820            create_test_liquidity_update(test_instrument_id, test_chain, test_dex, 200, 10, 7);
821        let pos = get_event_block_position(&DexPoolData::LiquidityUpdate(update));
822        assert_eq!(pos, (200, 10, 7));
823    }
824
825    #[rstest]
826    fn test_get_event_block_position_fee_collect(
827        test_instrument_id: InstrumentId,
828        test_chain: Arc<Chain>,
829        test_dex: Arc<Dex>,
830    ) {
831        let collect = create_test_fee_collect(test_instrument_id, test_chain, test_dex, 300, 15, 2);
832        let pos = get_event_block_position(&DexPoolData::FeeCollect(collect));
833        assert_eq!(pos, (300, 15, 2));
834    }
835
836    #[rstest]
837    fn test_get_event_block_position_flash(
838        test_instrument_id: InstrumentId,
839        test_chain: Arc<Chain>,
840        test_dex: Arc<Dex>,
841    ) {
842        let flash = create_test_flash(test_instrument_id, test_chain, test_dex, 400, 20, 8);
843        let pos = get_event_block_position(&DexPoolData::Flash(flash));
844        assert_eq!(pos, (400, 20, 8));
845    }
846
847    #[rstest]
848    fn test_get_event_block_position_fee_protocol_update(
849        test_instrument_id: InstrumentId,
850        test_chain: Arc<Chain>,
851        test_dex: Arc<Dex>,
852    ) {
853        let update =
854            create_test_fee_protocol_update(test_instrument_id, test_chain, test_dex, 500, 25, 4);
855        let pos = get_event_block_position(&DexPoolData::FeeProtocolUpdate(update));
856        assert_eq!(pos, (500, 25, 4));
857    }
858
859    #[rstest]
860    fn test_convert_and_sort_empty_events() {
861        let events = convert_and_sort_buffered_events(vec![]);
862        assert!(events.is_empty());
863    }
864
865    #[rstest]
866    fn test_convert_and_sort_filters_non_pool_events(
867        test_instrument_id: InstrumentId,
868        test_chain: Arc<Chain>,
869        test_dex: Arc<Dex>,
870    ) {
871        let events = vec![
872            DefiData::PoolSwap(create_test_swap(
873                test_instrument_id,
874                test_chain,
875                test_dex,
876                100,
877                0,
878                0,
879            )),
880            // Block events would be filtered out
881        ];
882        let sorted = convert_and_sort_buffered_events(events);
883        assert_eq!(sorted.len(), 1);
884    }
885
886    #[rstest]
887    fn test_convert_and_sort_single_event(
888        test_instrument_id: InstrumentId,
889        test_chain: Arc<Chain>,
890        test_dex: Arc<Dex>,
891    ) {
892        let swap = create_test_swap(test_instrument_id, test_chain, test_dex, 100, 5, 3);
893        let events = vec![DefiData::PoolSwap(swap)];
894        let sorted = convert_and_sort_buffered_events(events);
895        assert_eq!(sorted.len(), 1);
896        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 3));
897    }
898
899    #[rstest]
900    fn test_convert_and_sort_already_sorted(
901        test_instrument_id: InstrumentId,
902        test_chain: Arc<Chain>,
903        test_dex: Arc<Dex>,
904    ) {
905        let events = vec![
906            DefiData::PoolSwap(create_test_swap(
907                test_instrument_id,
908                test_chain.clone(),
909                test_dex.clone(),
910                100,
911                0,
912                0,
913            )),
914            DefiData::PoolSwap(create_test_swap(
915                test_instrument_id,
916                test_chain.clone(),
917                test_dex.clone(),
918                100,
919                0,
920                1,
921            )),
922            DefiData::PoolSwap(create_test_swap(
923                test_instrument_id,
924                test_chain,
925                test_dex,
926                100,
927                1,
928                0,
929            )),
930        ];
931        let sorted = convert_and_sort_buffered_events(events);
932        assert_eq!(sorted.len(), 3);
933        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 0));
934        assert_eq!(get_event_block_position(&sorted[1]), (100, 0, 1));
935        assert_eq!(get_event_block_position(&sorted[2]), (100, 1, 0));
936    }
937
938    #[rstest]
939    fn test_convert_and_sort_reverse_order(
940        test_instrument_id: InstrumentId,
941        test_chain: Arc<Chain>,
942        test_dex: Arc<Dex>,
943    ) {
944        let events = vec![
945            DefiData::PoolSwap(create_test_swap(
946                test_instrument_id,
947                test_chain.clone(),
948                test_dex.clone(),
949                100,
950                2,
951                5,
952            )),
953            DefiData::PoolSwap(create_test_swap(
954                test_instrument_id,
955                test_chain.clone(),
956                test_dex.clone(),
957                100,
958                1,
959                3,
960            )),
961            DefiData::PoolSwap(create_test_swap(
962                test_instrument_id,
963                test_chain,
964                test_dex,
965                100,
966                0,
967                1,
968            )),
969        ];
970        let sorted = convert_and_sort_buffered_events(events);
971        assert_eq!(sorted.len(), 3);
972        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 1));
973        assert_eq!(get_event_block_position(&sorted[1]), (100, 1, 3));
974        assert_eq!(get_event_block_position(&sorted[2]), (100, 2, 5));
975    }
976
977    #[rstest]
978    fn test_convert_and_sort_mixed_blocks(
979        test_instrument_id: InstrumentId,
980        test_chain: Arc<Chain>,
981        test_dex: Arc<Dex>,
982    ) {
983        let events = vec![
984            DefiData::PoolSwap(create_test_swap(
985                test_instrument_id,
986                test_chain.clone(),
987                test_dex.clone(),
988                102,
989                0,
990                0,
991            )),
992            DefiData::PoolSwap(create_test_swap(
993                test_instrument_id,
994                test_chain.clone(),
995                test_dex.clone(),
996                100,
997                5,
998                2,
999            )),
1000            DefiData::PoolSwap(create_test_swap(
1001                test_instrument_id,
1002                test_chain,
1003                test_dex,
1004                101,
1005                3,
1006                1,
1007            )),
1008        ];
1009        let sorted = convert_and_sort_buffered_events(events);
1010        assert_eq!(sorted.len(), 3);
1011        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 2));
1012        assert_eq!(get_event_block_position(&sorted[1]), (101, 3, 1));
1013        assert_eq!(get_event_block_position(&sorted[2]), (102, 0, 0));
1014    }
1015
1016    #[rstest]
1017    fn test_convert_and_sort_mixed_event_types(
1018        test_instrument_id: InstrumentId,
1019        test_chain: Arc<Chain>,
1020        test_dex: Arc<Dex>,
1021    ) {
1022        let events = vec![
1023            DefiData::PoolSwap(create_test_swap(
1024                test_instrument_id,
1025                test_chain.clone(),
1026                test_dex.clone(),
1027                100,
1028                2,
1029                0,
1030            )),
1031            DefiData::PoolLiquidityUpdate(create_test_liquidity_update(
1032                test_instrument_id,
1033                test_chain.clone(),
1034                test_dex.clone(),
1035                100,
1036                0,
1037                0,
1038            )),
1039            DefiData::PoolFeeCollect(create_test_fee_collect(
1040                test_instrument_id,
1041                test_chain.clone(),
1042                test_dex.clone(),
1043                100,
1044                1,
1045                0,
1046            )),
1047            DefiData::PoolFlash(create_test_flash(
1048                test_instrument_id,
1049                test_chain.clone(),
1050                test_dex.clone(),
1051                100,
1052                3,
1053                0,
1054            )),
1055            DefiData::PoolFeeProtocolUpdate(create_test_fee_protocol_update(
1056                test_instrument_id,
1057                test_chain,
1058                test_dex,
1059                100,
1060                4,
1061                0,
1062            )),
1063        ];
1064        let sorted = convert_and_sort_buffered_events(events);
1065        assert_eq!(sorted.len(), 5);
1066        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 0));
1067        assert_eq!(get_event_block_position(&sorted[1]), (100, 1, 0));
1068        assert_eq!(get_event_block_position(&sorted[2]), (100, 2, 0));
1069        assert_eq!(get_event_block_position(&sorted[3]), (100, 3, 0));
1070        assert_eq!(get_event_block_position(&sorted[4]), (100, 4, 0));
1071        assert!(matches!(sorted[4], DexPoolData::FeeProtocolUpdate(_)));
1072    }
1073
1074    #[rstest]
1075    fn test_convert_and_sort_same_block_and_tx_different_log_index(
1076        test_instrument_id: InstrumentId,
1077        test_chain: Arc<Chain>,
1078        test_dex: Arc<Dex>,
1079    ) {
1080        let events = vec![
1081            DefiData::PoolSwap(create_test_swap(
1082                test_instrument_id,
1083                test_chain.clone(),
1084                test_dex.clone(),
1085                100,
1086                5,
1087                10,
1088            )),
1089            DefiData::PoolSwap(create_test_swap(
1090                test_instrument_id,
1091                test_chain.clone(),
1092                test_dex.clone(),
1093                100,
1094                5,
1095                5,
1096            )),
1097            DefiData::PoolSwap(create_test_swap(
1098                test_instrument_id,
1099                test_chain,
1100                test_dex,
1101                100,
1102                5,
1103                1,
1104            )),
1105        ];
1106        let sorted = convert_and_sort_buffered_events(events);
1107        assert_eq!(sorted.len(), 3);
1108        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 1));
1109        assert_eq!(get_event_block_position(&sorted[1]), (100, 5, 5));
1110        assert_eq!(get_event_block_position(&sorted[2]), (100, 5, 10));
1111    }
1112}