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.is_external_client(*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_with_retained(cmd.clone(), true);
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.is_external_client(*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.is_external_client(*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
588                let request = RequestPoolSnapshot::new(
589                    *instrument_id,
590                    client_id.copied(),
591                    request_id,
592                    ts_init,
593                    None,
594                );
595
596                if let Err(e) = self.execute_defi_request(DefiRequestCommand::PoolSnapshot(request))
597                {
598                    log::warn!("Failed to request pool snapshot for {instrument_id}: {e}");
599                } else {
600                    log::debug!("Requested pool snapshot for {instrument_id}");
601                    self.pool_snapshot_pending.insert(*instrument_id);
602                    self.pool_updaters_pending.insert(*instrument_id);
603                    self.pool_event_buffers.entry(*instrument_id).or_default();
604                }
605                return;
606            }
607        }
608
609        // Profiler exists, create updater and subscribe to topics
610        let updater = Rc::new(PoolUpdater::new(instrument_id, self.cache().clone()));
611
612        self.subscribe_pool_updater_topics(*instrument_id, updater.clone());
613        self.pool_updaters.insert(*instrument_id, updater);
614
615        log::debug!("Created PoolUpdater for instrument ID {instrument_id}");
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use std::sync::Arc;
622
623    use alloy_primitives::{Address, I256, U160, U256};
624    use nautilus_core::UnixNanos;
625    use nautilus_model::{
626        defi::{
627            Chain, DefiData, PoolFeeCollect, PoolFeeProtocolUpdate, PoolFlash, PoolIdentifier,
628            PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap,
629            chain::chains,
630            data::DexPoolData,
631            dex::{AmmType, Dex, DexType},
632        },
633        identifiers::{InstrumentId, Symbol, Venue},
634    };
635    use rstest::*;
636
637    use super::*;
638
639    #[fixture]
640    fn test_instrument_id() -> InstrumentId {
641        InstrumentId::new(Symbol::from("ETH/USDC"), Venue::from("UNISWAPV3"))
642    }
643
644    #[fixture]
645    fn test_chain() -> Arc<Chain> {
646        Arc::new(chains::ETHEREUM.clone())
647    }
648
649    #[fixture]
650    fn test_dex(test_chain: Arc<Chain>) -> Arc<Dex> {
651        Arc::new(Dex::new(
652            (*test_chain).clone(),
653            DexType::UniswapV3,
654            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
655            12369621,
656            AmmType::CLAMM,
657            "PoolCreated(address,address,uint24,int24,address)",
658            "Swap(address,address,int256,int256,uint160,uint128,int24)",
659            "Mint(address,address,int24,int24,uint128,uint256,uint256)",
660            "Burn(address,int24,int24,uint128,uint256,uint256)",
661            "Collect(address,address,int24,int24,uint128,uint128)",
662        ))
663    }
664
665    fn create_test_swap(
666        test_instrument_id: InstrumentId,
667        test_chain: Arc<Chain>,
668        test_dex: Arc<Dex>,
669        block: u64,
670        tx_index: u32,
671        log_index: u32,
672    ) -> PoolSwap {
673        PoolSwap::new(
674            test_chain,
675            test_dex,
676            test_instrument_id,
677            PoolIdentifier::from_address(Address::ZERO),
678            block,
679            format!("0x{block:064x}"),
680            tx_index,
681            log_index,
682            UnixNanos::default(),
683            UnixNanos::default(),
684            Address::ZERO,
685            Address::ZERO,
686            I256::ZERO,
687            I256::ZERO,
688            U160::ZERO,
689            0,
690            0,
691        )
692    }
693
694    fn create_test_liquidity_update(
695        test_instrument_id: InstrumentId,
696        test_chain: Arc<Chain>,
697        test_dex: Arc<Dex>,
698        block: u64,
699        tx_index: u32,
700        log_index: u32,
701    ) -> PoolLiquidityUpdate {
702        PoolLiquidityUpdate::new(
703            test_chain,
704            test_dex,
705            test_instrument_id,
706            PoolIdentifier::from_address(Address::ZERO),
707            PoolLiquidityUpdateType::Mint,
708            block,
709            format!("0x{block:064x}"),
710            tx_index,
711            log_index,
712            None,
713            Address::ZERO,
714            0,
715            U256::ZERO,
716            U256::ZERO,
717            0,
718            0,
719            UnixNanos::default(),
720            UnixNanos::default(),
721        )
722    }
723
724    fn create_test_fee_collect(
725        test_instrument_id: InstrumentId,
726        test_chain: Arc<Chain>,
727        test_dex: Arc<Dex>,
728        block: u64,
729        tx_index: u32,
730        log_index: u32,
731    ) -> PoolFeeCollect {
732        PoolFeeCollect::new(
733            test_chain,
734            test_dex,
735            test_instrument_id,
736            PoolIdentifier::from_address(Address::ZERO),
737            block,
738            format!("0x{block:064x}"),
739            tx_index,
740            log_index,
741            Address::ZERO,
742            0,
743            0,
744            0,
745            0,
746            UnixNanos::default(),
747            UnixNanos::default(),
748        )
749    }
750
751    fn create_test_flash(
752        test_instrument_id: InstrumentId,
753        test_chain: Arc<Chain>,
754        test_dex: Arc<Dex>,
755        block: u64,
756        tx_index: u32,
757        log_index: u32,
758    ) -> PoolFlash {
759        PoolFlash::new(
760            test_chain,
761            test_dex,
762            test_instrument_id,
763            PoolIdentifier::from_address(Address::ZERO),
764            block,
765            format!("0x{block:064x}"),
766            tx_index,
767            log_index,
768            UnixNanos::default(),
769            UnixNanos::default(),
770            Address::ZERO,
771            Address::ZERO,
772            U256::ZERO,
773            U256::ZERO,
774            U256::ZERO,
775            U256::ZERO,
776        )
777    }
778
779    fn create_test_fee_protocol_update(
780        test_instrument_id: InstrumentId,
781        test_chain: Arc<Chain>,
782        test_dex: Arc<Dex>,
783        block: u64,
784        tx_index: u32,
785        log_index: u32,
786    ) -> PoolFeeProtocolUpdate {
787        PoolFeeProtocolUpdate::new(
788            test_chain,
789            test_dex,
790            test_instrument_id,
791            PoolIdentifier::from_address(Address::ZERO),
792            block,
793            format!("0x{block:064x}"),
794            tx_index,
795            log_index,
796            4,
797            4,
798            UnixNanos::default(),
799            UnixNanos::default(),
800        )
801    }
802
803    #[rstest]
804    fn test_get_event_block_position_swap(
805        test_instrument_id: InstrumentId,
806        test_chain: Arc<Chain>,
807        test_dex: Arc<Dex>,
808    ) {
809        let swap = create_test_swap(test_instrument_id, test_chain, test_dex, 100, 5, 3);
810        let pos = get_event_block_position(&DexPoolData::Swap(swap));
811        assert_eq!(pos, (100, 5, 3));
812    }
813
814    #[rstest]
815    fn test_get_event_block_position_liquidity_update(
816        test_instrument_id: InstrumentId,
817        test_chain: Arc<Chain>,
818        test_dex: Arc<Dex>,
819    ) {
820        let update =
821            create_test_liquidity_update(test_instrument_id, test_chain, test_dex, 200, 10, 7);
822        let pos = get_event_block_position(&DexPoolData::LiquidityUpdate(update));
823        assert_eq!(pos, (200, 10, 7));
824    }
825
826    #[rstest]
827    fn test_get_event_block_position_fee_collect(
828        test_instrument_id: InstrumentId,
829        test_chain: Arc<Chain>,
830        test_dex: Arc<Dex>,
831    ) {
832        let collect = create_test_fee_collect(test_instrument_id, test_chain, test_dex, 300, 15, 2);
833        let pos = get_event_block_position(&DexPoolData::FeeCollect(collect));
834        assert_eq!(pos, (300, 15, 2));
835    }
836
837    #[rstest]
838    fn test_get_event_block_position_flash(
839        test_instrument_id: InstrumentId,
840        test_chain: Arc<Chain>,
841        test_dex: Arc<Dex>,
842    ) {
843        let flash = create_test_flash(test_instrument_id, test_chain, test_dex, 400, 20, 8);
844        let pos = get_event_block_position(&DexPoolData::Flash(flash));
845        assert_eq!(pos, (400, 20, 8));
846    }
847
848    #[rstest]
849    fn test_get_event_block_position_fee_protocol_update(
850        test_instrument_id: InstrumentId,
851        test_chain: Arc<Chain>,
852        test_dex: Arc<Dex>,
853    ) {
854        let update =
855            create_test_fee_protocol_update(test_instrument_id, test_chain, test_dex, 500, 25, 4);
856        let pos = get_event_block_position(&DexPoolData::FeeProtocolUpdate(update));
857        assert_eq!(pos, (500, 25, 4));
858    }
859
860    #[rstest]
861    fn test_convert_and_sort_empty_events() {
862        let events = convert_and_sort_buffered_events(vec![]);
863        assert!(events.is_empty());
864    }
865
866    #[rstest]
867    fn test_convert_and_sort_filters_non_pool_events(
868        test_instrument_id: InstrumentId,
869        test_chain: Arc<Chain>,
870        test_dex: Arc<Dex>,
871    ) {
872        let events = vec![
873            DefiData::PoolSwap(create_test_swap(
874                test_instrument_id,
875                test_chain,
876                test_dex,
877                100,
878                0,
879                0,
880            )),
881            // Block events would be filtered out
882        ];
883        let sorted = convert_and_sort_buffered_events(events);
884        assert_eq!(sorted.len(), 1);
885    }
886
887    #[rstest]
888    fn test_convert_and_sort_single_event(
889        test_instrument_id: InstrumentId,
890        test_chain: Arc<Chain>,
891        test_dex: Arc<Dex>,
892    ) {
893        let swap = create_test_swap(test_instrument_id, test_chain, test_dex, 100, 5, 3);
894        let events = vec![DefiData::PoolSwap(swap)];
895        let sorted = convert_and_sort_buffered_events(events);
896        assert_eq!(sorted.len(), 1);
897        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 3));
898    }
899
900    #[rstest]
901    fn test_convert_and_sort_already_sorted(
902        test_instrument_id: InstrumentId,
903        test_chain: Arc<Chain>,
904        test_dex: Arc<Dex>,
905    ) {
906        let events = vec![
907            DefiData::PoolSwap(create_test_swap(
908                test_instrument_id,
909                test_chain.clone(),
910                test_dex.clone(),
911                100,
912                0,
913                0,
914            )),
915            DefiData::PoolSwap(create_test_swap(
916                test_instrument_id,
917                test_chain.clone(),
918                test_dex.clone(),
919                100,
920                0,
921                1,
922            )),
923            DefiData::PoolSwap(create_test_swap(
924                test_instrument_id,
925                test_chain,
926                test_dex,
927                100,
928                1,
929                0,
930            )),
931        ];
932        let sorted = convert_and_sort_buffered_events(events);
933        assert_eq!(sorted.len(), 3);
934        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 0));
935        assert_eq!(get_event_block_position(&sorted[1]), (100, 0, 1));
936        assert_eq!(get_event_block_position(&sorted[2]), (100, 1, 0));
937    }
938
939    #[rstest]
940    fn test_convert_and_sort_reverse_order(
941        test_instrument_id: InstrumentId,
942        test_chain: Arc<Chain>,
943        test_dex: Arc<Dex>,
944    ) {
945        let events = vec![
946            DefiData::PoolSwap(create_test_swap(
947                test_instrument_id,
948                test_chain.clone(),
949                test_dex.clone(),
950                100,
951                2,
952                5,
953            )),
954            DefiData::PoolSwap(create_test_swap(
955                test_instrument_id,
956                test_chain.clone(),
957                test_dex.clone(),
958                100,
959                1,
960                3,
961            )),
962            DefiData::PoolSwap(create_test_swap(
963                test_instrument_id,
964                test_chain,
965                test_dex,
966                100,
967                0,
968                1,
969            )),
970        ];
971        let sorted = convert_and_sort_buffered_events(events);
972        assert_eq!(sorted.len(), 3);
973        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 1));
974        assert_eq!(get_event_block_position(&sorted[1]), (100, 1, 3));
975        assert_eq!(get_event_block_position(&sorted[2]), (100, 2, 5));
976    }
977
978    #[rstest]
979    fn test_convert_and_sort_mixed_blocks(
980        test_instrument_id: InstrumentId,
981        test_chain: Arc<Chain>,
982        test_dex: Arc<Dex>,
983    ) {
984        let events = vec![
985            DefiData::PoolSwap(create_test_swap(
986                test_instrument_id,
987                test_chain.clone(),
988                test_dex.clone(),
989                102,
990                0,
991                0,
992            )),
993            DefiData::PoolSwap(create_test_swap(
994                test_instrument_id,
995                test_chain.clone(),
996                test_dex.clone(),
997                100,
998                5,
999                2,
1000            )),
1001            DefiData::PoolSwap(create_test_swap(
1002                test_instrument_id,
1003                test_chain,
1004                test_dex,
1005                101,
1006                3,
1007                1,
1008            )),
1009        ];
1010        let sorted = convert_and_sort_buffered_events(events);
1011        assert_eq!(sorted.len(), 3);
1012        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 2));
1013        assert_eq!(get_event_block_position(&sorted[1]), (101, 3, 1));
1014        assert_eq!(get_event_block_position(&sorted[2]), (102, 0, 0));
1015    }
1016
1017    #[rstest]
1018    fn test_convert_and_sort_mixed_event_types(
1019        test_instrument_id: InstrumentId,
1020        test_chain: Arc<Chain>,
1021        test_dex: Arc<Dex>,
1022    ) {
1023        let events = vec![
1024            DefiData::PoolSwap(create_test_swap(
1025                test_instrument_id,
1026                test_chain.clone(),
1027                test_dex.clone(),
1028                100,
1029                2,
1030                0,
1031            )),
1032            DefiData::PoolLiquidityUpdate(create_test_liquidity_update(
1033                test_instrument_id,
1034                test_chain.clone(),
1035                test_dex.clone(),
1036                100,
1037                0,
1038                0,
1039            )),
1040            DefiData::PoolFeeCollect(create_test_fee_collect(
1041                test_instrument_id,
1042                test_chain.clone(),
1043                test_dex.clone(),
1044                100,
1045                1,
1046                0,
1047            )),
1048            DefiData::PoolFlash(create_test_flash(
1049                test_instrument_id,
1050                test_chain.clone(),
1051                test_dex.clone(),
1052                100,
1053                3,
1054                0,
1055            )),
1056            DefiData::PoolFeeProtocolUpdate(create_test_fee_protocol_update(
1057                test_instrument_id,
1058                test_chain,
1059                test_dex,
1060                100,
1061                4,
1062                0,
1063            )),
1064        ];
1065        let sorted = convert_and_sort_buffered_events(events);
1066        assert_eq!(sorted.len(), 5);
1067        assert_eq!(get_event_block_position(&sorted[0]), (100, 0, 0));
1068        assert_eq!(get_event_block_position(&sorted[1]), (100, 1, 0));
1069        assert_eq!(get_event_block_position(&sorted[2]), (100, 2, 0));
1070        assert_eq!(get_event_block_position(&sorted[3]), (100, 3, 0));
1071        assert_eq!(get_event_block_position(&sorted[4]), (100, 4, 0));
1072        assert!(matches!(sorted[4], DexPoolData::FeeProtocolUpdate(_)));
1073    }
1074
1075    #[rstest]
1076    fn test_convert_and_sort_same_block_and_tx_different_log_index(
1077        test_instrument_id: InstrumentId,
1078        test_chain: Arc<Chain>,
1079        test_dex: Arc<Dex>,
1080    ) {
1081        let events = vec![
1082            DefiData::PoolSwap(create_test_swap(
1083                test_instrument_id,
1084                test_chain.clone(),
1085                test_dex.clone(),
1086                100,
1087                5,
1088                10,
1089            )),
1090            DefiData::PoolSwap(create_test_swap(
1091                test_instrument_id,
1092                test_chain.clone(),
1093                test_dex.clone(),
1094                100,
1095                5,
1096                5,
1097            )),
1098            DefiData::PoolSwap(create_test_swap(
1099                test_instrument_id,
1100                test_chain,
1101                test_dex,
1102                100,
1103                5,
1104                1,
1105            )),
1106        ];
1107        let sorted = convert_and_sort_buffered_events(events);
1108        assert_eq!(sorted.len(), 3);
1109        assert_eq!(get_event_block_position(&sorted[0]), (100, 5, 1));
1110        assert_eq!(get_event_block_position(&sorted[1]), (100, 5, 5));
1111        assert_eq!(get_event_block_position(&sorted[2]), (100, 5, 10));
1112    }
1113}