Skip to main content

odos_sdk/
events.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Event monitoring utilities for tracking Odos swaps.
6//!
7//! This module provides utilities for querying and filtering swap events from the
8//! Odos router contracts. It uses Alloy's log filtering capabilities for efficient
9//! event retrieval.
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use odos_sdk::events::{SwapEventFilter, SwapEvent};
15//! use alloy_provider::ProviderBuilder;
16//! use alloy_primitives::address;
17//!
18//! let provider = ProviderBuilder::new()
19//!     .connect_http("https://eth.llamarpc.com".parse()?);
20//!
21//! // Create a filter for recent swaps
22//! let filter = SwapEventFilter::new(router_address)
23//!     .from_block(18_000_000)
24//!     .sender(my_address);
25//!
26//! // Get swap events
27//! let events = filter.get_events(&provider).await?;
28//! for event in events {
29//!     println!("Swap: {} {} -> {} {}",
30//!         event.input_amount, event.input_token,
31//!         event.amount_out, event.output_token);
32//! }
33//! ```
34
35use alloy_network::Network;
36use alloy_primitives::{Address, B256, I256, U256};
37use alloy_provider::Provider;
38use alloy_rpc_types::{BlockNumberOrTag, Filter, Log};
39use alloy_sol_types::SolEvent;
40
41#[cfg(feature = "v2")]
42use crate::v2_router::OdosV2Router;
43#[cfg(feature = "v3")]
44use crate::v3_router::OdosV3Router;
45
46/// A decoded swap event from an Odos router contract.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct SwapEvent {
49    /// The sender of the swap.
50    pub sender: Address,
51    /// The input token address.
52    pub input_token: Address,
53    /// The input amount.
54    pub input_amount: U256,
55    /// The output token address.
56    pub output_token: Address,
57    /// The output amount received.
58    pub amount_out: U256,
59    /// Slippage as a signed 256-bit integer (positive = more output than expected).
60    pub slippage: I256,
61    /// Referral code used (if any).
62    pub referral_code: u64,
63    /// The block number where the swap occurred.
64    pub block_number: Option<u64>,
65    /// The transaction hash.
66    pub transaction_hash: Option<B256>,
67    /// The log index within the transaction.
68    pub log_index: Option<u64>,
69}
70
71/// Builder for creating swap event filters.
72///
73/// This builder provides a fluent API for constructing filters to query
74/// swap events from Odos router contracts.
75///
76/// # Example
77///
78/// ```rust,ignore
79/// use odos_sdk::events::SwapEventFilter;
80///
81/// let filter = SwapEventFilter::new(router_address)
82///     .from_block(18_000_000)
83///     .to_block(18_100_000)
84///     .sender(sender_address);
85/// ```
86#[derive(Debug, Clone)]
87pub struct SwapEventFilter {
88    router_address: Address,
89    from_block: Option<BlockNumberOrTag>,
90    to_block: Option<BlockNumberOrTag>,
91    sender: Option<Address>,
92}
93
94impl SwapEventFilter {
95    /// Creates a new swap event filter for the given router address.
96    pub fn new(router_address: Address) -> Self {
97        Self {
98            router_address,
99            from_block: None,
100            to_block: None,
101            sender: None,
102        }
103    }
104
105    /// Sets the starting block for the filter.
106    pub fn from_block(mut self, block: u64) -> Self {
107        self.from_block = Some(BlockNumberOrTag::Number(block));
108        self
109    }
110
111    /// Sets the starting block to the latest block.
112    pub fn from_latest(mut self) -> Self {
113        self.from_block = Some(BlockNumberOrTag::Latest);
114        self
115    }
116
117    /// Sets the ending block for the filter.
118    pub fn to_block(mut self, block: u64) -> Self {
119        self.to_block = Some(BlockNumberOrTag::Number(block));
120        self
121    }
122
123    /// Sets the ending block to the latest block.
124    pub fn to_latest(mut self) -> Self {
125        self.to_block = Some(BlockNumberOrTag::Latest);
126        self
127    }
128
129    /// Filters events by sender address.
130    pub fn sender(mut self, sender: Address) -> Self {
131        self.sender = Some(sender);
132        self
133    }
134
135    /// Builds the underlying Alloy filter for V2 Swap events.
136    #[cfg(feature = "v2")]
137    pub fn build_v2_filter(&self) -> Filter {
138        let mut filter = Filter::new()
139            .address(self.router_address)
140            .event_signature(OdosV2Router::Swap::SIGNATURE_HASH);
141
142        if let Some(from) = self.from_block {
143            filter = filter.from_block(from);
144        }
145
146        if let Some(to) = self.to_block {
147            filter = filter.to_block(to);
148        }
149
150        // Note: Swap events have indexed sender, so we can filter on it
151        if let Some(sender) = self.sender {
152            filter = filter.topic1(sender.into_word());
153        }
154
155        filter
156    }
157
158    /// Builds the underlying Alloy filter for V3 Swap events.
159    #[cfg(feature = "v3")]
160    pub fn build_v3_filter(&self) -> Filter {
161        let mut filter = Filter::new()
162            .address(self.router_address)
163            .event_signature(OdosV3Router::Swap::SIGNATURE_HASH);
164
165        if let Some(from) = self.from_block {
166            filter = filter.from_block(from);
167        }
168
169        if let Some(to) = self.to_block {
170            filter = filter.to_block(to);
171        }
172
173        if let Some(sender) = self.sender {
174            filter = filter.topic1(sender.into_word());
175        }
176
177        filter
178    }
179
180    /// Gets V2 swap events from the provider.
181    #[cfg(feature = "v2")]
182    pub async fn get_v2_events<N, P>(
183        &self,
184        provider: &P,
185    ) -> Result<Vec<SwapEvent>, alloy_transport::TransportError>
186    where
187        N: Network,
188        P: Provider<N>,
189    {
190        let filter = self.build_v2_filter();
191        let logs = provider.get_logs(&filter).await?;
192
193        Ok(logs
194            .into_iter()
195            .filter_map(|log| Self::decode_v2_swap_log(&log))
196            .collect())
197    }
198
199    /// Gets V3 swap events from the provider.
200    #[cfg(feature = "v3")]
201    pub async fn get_v3_events<N, P>(
202        &self,
203        provider: &P,
204    ) -> Result<Vec<SwapEvent>, alloy_transport::TransportError>
205    where
206        N: Network,
207        P: Provider<N>,
208    {
209        let filter = self.build_v3_filter();
210        let logs = provider.get_logs(&filter).await?;
211
212        Ok(logs
213            .into_iter()
214            .filter_map(|log| Self::decode_v3_swap_log(&log))
215            .collect())
216    }
217
218    /// Decodes a V2 Swap event from a log.
219    #[cfg(feature = "v2")]
220    fn decode_v2_swap_log(log: &Log) -> Option<SwapEvent> {
221        let decoded = OdosV2Router::Swap::decode_log(&log.inner).ok()?;
222        Some(SwapEvent {
223            sender: decoded.data.sender,
224            input_token: decoded.data.inputToken,
225            input_amount: decoded.data.inputAmount,
226            output_token: decoded.data.outputToken,
227            amount_out: decoded.data.amountOut,
228            slippage: decoded.data.slippage,
229            referral_code: u64::from(decoded.data.referralCode),
230            block_number: log.block_number,
231            transaction_hash: log.transaction_hash,
232            log_index: log.log_index,
233        })
234    }
235
236    /// Decodes a V3 Swap event from a log.
237    #[cfg(feature = "v3")]
238    fn decode_v3_swap_log(log: &Log) -> Option<SwapEvent> {
239        let decoded = OdosV3Router::Swap::decode_log(&log.inner).ok()?;
240        Some(SwapEvent {
241            sender: decoded.data.sender,
242            input_token: decoded.data.inputToken,
243            input_amount: decoded.data.inputAmount,
244            output_token: decoded.data.outputToken,
245            amount_out: decoded.data.amountOut,
246            slippage: decoded.data.slippage,
247            referral_code: decoded.data.referralCode,
248            block_number: log.block_number,
249            transaction_hash: log.transaction_hash,
250            log_index: log.log_index,
251        })
252    }
253}
254
255/// A decoded multi-token swap event from an Odos router contract.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct SwapMultiEvent {
258    /// The sender of the swap.
259    pub sender: Address,
260    /// The input token addresses.
261    pub tokens_in: Vec<Address>,
262    /// The input amounts.
263    pub amounts_in: Vec<U256>,
264    /// The output token addresses.
265    pub tokens_out: Vec<Address>,
266    /// The output amounts received.
267    pub amounts_out: Vec<U256>,
268    /// The block number where the swap occurred.
269    pub block_number: Option<u64>,
270    /// The transaction hash.
271    pub transaction_hash: Option<B256>,
272    /// The log index within the transaction.
273    pub log_index: Option<u64>,
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use alloy_primitives::address;
280
281    #[test]
282    fn test_swap_event_filter_builder() {
283        let router = address!("cf5540fffcdc3d510b18bfca6d2b9987b0772559");
284        let sender = address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0");
285
286        let filter = SwapEventFilter::new(router)
287            .from_block(18_000_000)
288            .to_block(18_100_000)
289            .sender(sender);
290
291        assert_eq!(filter.router_address, router);
292        assert_eq!(
293            filter.from_block,
294            Some(BlockNumberOrTag::Number(18_000_000))
295        );
296        assert_eq!(filter.to_block, Some(BlockNumberOrTag::Number(18_100_000)));
297        assert_eq!(filter.sender, Some(sender));
298    }
299
300    #[test]
301    fn test_swap_event_filter_latest_blocks() {
302        let router = address!("cf5540fffcdc3d510b18bfca6d2b9987b0772559");
303
304        let filter = SwapEventFilter::new(router).from_latest().to_latest();
305
306        assert_eq!(filter.from_block, Some(BlockNumberOrTag::Latest));
307        assert_eq!(filter.to_block, Some(BlockNumberOrTag::Latest));
308    }
309
310    #[cfg(feature = "v3")]
311    #[test]
312    fn test_build_v3_filter() {
313        let router = address!("cf5540fffcdc3d510b18bfca6d2b9987b0772559");
314
315        let filter = SwapEventFilter::new(router)
316            .from_block(18_000_000)
317            .build_v3_filter();
318
319        // Verify the filter was built successfully (non-empty address)
320        // The filter should be valid - we just verify it builds without error
321        let _ = filter;
322    }
323}