Skip to main content

nautilus_model/defi/
dex.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
16use std::{borrow::Cow, fmt::Display, str::FromStr, sync::Arc};
17
18use alloy_primitives::{Address, keccak256};
19use nautilus_core::hex;
20use serde::{Deserialize, Serialize};
21use strum::{Display, EnumIter, EnumString};
22
23use crate::{
24    defi::{amm::Pool, chain::Chain, validation::validate_address},
25    identifiers::{InstrumentId, Symbol, Venue},
26    instruments::{Instrument, any::InstrumentAny, currency_pair::CurrencyPair},
27    types::{currency::Currency, fixed::FIXED_PRECISION, price::Price, quantity::Quantity},
28};
29
30/// Represents different types of Automated Market Makers (AMMs) in DeFi protocols.
31#[derive(
32    Debug,
33    Clone,
34    Copy,
35    Hash,
36    PartialEq,
37    Eq,
38    Serialize,
39    Deserialize,
40    strum::EnumString,
41    strum::Display,
42    strum::EnumIter,
43)]
44#[cfg_attr(
45    feature = "python",
46    pyo3::pyclass(
47        frozen,
48        eq,
49        eq_int,
50        module = "nautilus_trader.model",
51        from_py_object,
52        rename_all = "SCREAMING_SNAKE_CASE",
53    )
54)]
55#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
56#[non_exhaustive]
57pub enum AmmType {
58    /// Constant Product Automated Market Maker.
59    CPAMM,
60    /// Concentrated Liquidity Automated Market Maker.
61    CLAMM,
62    /// Concentrated liquidity AMM **with hooks** (e.g. upcoming Uniswap v4).
63    CLAMEnhanced,
64    /// Specialized Constant-Sum AMM for low-volatility assets (Curve-style “`StableSwap`”).
65    StableSwap,
66    /// AMM with customizable token weights (e.g., Balancer style).
67    WeightedPool,
68    /// Advanced pool type that can nest other pools (Balancer V3).
69    ComposablePool,
70}
71
72/// Represents different types of decentralized exchanges (DEXes) supported by Nautilus.
73#[derive(
74    Debug,
75    Clone,
76    Copy,
77    Hash,
78    PartialOrd,
79    PartialEq,
80    Ord,
81    Eq,
82    Display,
83    EnumIter,
84    EnumString,
85    Serialize,
86    Deserialize,
87)]
88#[cfg_attr(
89    feature = "python",
90    pyo3::pyclass(
91        frozen,
92        eq,
93        eq_int,
94        module = "nautilus_trader.model",
95        from_py_object,
96        rename_all = "SCREAMING_SNAKE_CASE",
97    )
98)]
99#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
100pub enum DexType {
101    AerodromeSlipstream,
102    AerodromeV1,
103    BalancerV2,
104    BalancerV3,
105    BaseSwapV2,
106    BaseX,
107    CamelotV3,
108    CurveFinance,
109    FluidDEX,
110    MaverickV1,
111    MaverickV2,
112    PancakeSwapV3,
113    SushiSwapV2,
114    SushiSwapV3,
115    UniswapV2,
116    UniswapV3,
117    UniswapV4,
118}
119
120impl DexType {
121    /// Returns a reference to the `DexType` corresponding to the given dex name, or `None` if it is not found.
122    #[must_use]
123    pub fn from_dex_name(dex_name: &str) -> Option<Self> {
124        Self::from_str(dex_name).ok()
125    }
126}
127
128/// Represents a decentralized exchange (DEX) in a blockchain ecosystem.
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130#[cfg_attr(
131    feature = "python",
132    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
133)]
134#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
135pub struct Dex {
136    /// The blockchain network where this DEX operates.
137    pub chain: Chain,
138    /// The variant of the DEX protocol.
139    pub name: DexType,
140    /// The blockchain address of the DEX factory contract.
141    pub factory: Address,
142    /// The block number at which the DEX factory contract was deployed.
143    pub factory_creation_block: u64,
144    /// The event signature or identifier used to detect pool creation events.
145    pub pool_created_event: Cow<'static, str>,
146    // Optional Initialize event signature emitted when pool is initialized.
147    pub initialize_event: Option<Cow<'static, str>>,
148    /// The event signature or identifier used to detect swap events.
149    pub swap_created_event: Cow<'static, str>,
150    /// The event signature or identifier used to detect mint events.
151    pub mint_created_event: Cow<'static, str>,
152    /// The event signature or identifier used to detect burn events.
153    pub burn_created_event: Cow<'static, str>,
154    /// The event signature or identifier used to detect collect fee events.
155    pub collect_created_event: Cow<'static, str>,
156    // Optional Flash event signature emitted when flash loan occurs.
157    pub flash_created_event: Option<Cow<'static, str>>,
158    /// The type of automated market maker (AMM) algorithm used by this DEX.
159    pub amm_type: AmmType,
160    /// Collection of liquidity pools managed by this DEX.
161    #[allow(dead_code)]
162    pairs: Vec<Pool>,
163}
164
165/// A thread-safe shared pointer to a `Dex`, enabling efficient reuse across multiple components.
166pub type SharedDex = Arc<Dex>;
167
168impl Dex {
169    /// Creates a new [`Dex`] instance with the specified properties.
170    ///
171    /// # Panics
172    ///
173    /// Panics if the provided factory address is invalid.
174    #[must_use]
175    #[expect(clippy::too_many_arguments)]
176    pub fn new(
177        chain: Chain,
178        name: DexType,
179        factory: &str,
180        factory_creation_block: u64,
181        amm_type: AmmType,
182        pool_created_event: &str,
183        swap_event: &str,
184        mint_event: &str,
185        burn_event: &str,
186        collect_event: &str,
187    ) -> Self {
188        let encoded_pool_created_event =
189            hex::encode_prefixed(keccak256(pool_created_event.as_bytes()));
190        let encoded_swap_event = hex::encode_prefixed(keccak256(swap_event.as_bytes()));
191        let encoded_mint_event = hex::encode_prefixed(keccak256(mint_event.as_bytes()));
192        let encoded_burn_event = hex::encode_prefixed(keccak256(burn_event.as_bytes()));
193        let encoded_collect_event = hex::encode_prefixed(keccak256(collect_event.as_bytes()));
194        let factory_address = match validate_address(factory) {
195            Ok(address) => address,
196            Err(e) => panic!(
197                "Invalid factory address for DEX {name} on chain {chain} for factory address {factory}: {e}"
198            ),
199        };
200        Self {
201            chain,
202            name,
203            factory: factory_address,
204            factory_creation_block,
205            pool_created_event: encoded_pool_created_event.into(),
206            initialize_event: None,
207            swap_created_event: encoded_swap_event.into(),
208            mint_created_event: encoded_mint_event.into(),
209            burn_created_event: encoded_burn_event.into(),
210            collect_created_event: encoded_collect_event.into(),
211            flash_created_event: None,
212            amm_type,
213            pairs: vec![],
214        }
215    }
216
217    /// Returns a unique identifier for this DEX, combining chain and protocol name.
218    #[must_use]
219    pub fn id(&self) -> String {
220        format!("{}:{}", self.chain.name, self.name)
221    }
222
223    /// Sets the pool initialization event signature by hashing and encoding the provided event string.
224    pub fn set_initialize_event(&mut self, event: &str) {
225        self.initialize_event = Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
226    }
227
228    /// Sets the flash loan event signature by hashing and encoding the provided event string.
229    pub fn set_flash_event(&mut self, event: &str) {
230        self.flash_created_event = Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
231    }
232}
233
234impl Display for Dex {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        write!(f, "Dex(chain={}, name={})", self.chain, self.name)
237    }
238}
239
240impl From<Pool> for CurrencyPair {
241    fn from(p: Pool) -> Self {
242        let symbol = Symbol::from(format!("{}/{}", p.token0.symbol, p.token1.symbol));
243        let id = InstrumentId::new(symbol, Venue::from(p.dex.id()));
244
245        let size_precision = p.token0.decimals.min(FIXED_PRECISION);
246        let price_precision = p.token1.decimals.min(FIXED_PRECISION);
247
248        let price_increment = Price::new(10f64.powi(-i32::from(price_precision)), price_precision);
249        let size_increment = Quantity::new(10f64.powi(-i32::from(size_precision)), size_precision);
250
251        Self::new(
252            id,
253            symbol,
254            Currency::from(p.token0.symbol.as_str()),
255            Currency::from(p.token1.symbol.as_str()),
256            price_precision,
257            size_precision,
258            price_increment,
259            size_increment,
260            None,
261            None,
262            None,
263            None,
264            None,
265            None,
266            None,
267            None,
268            None,
269            None,
270            None,
271            None,
272            None,
273            0.into(),
274            0.into(),
275        )
276    }
277}
278
279impl From<Pool> for InstrumentAny {
280    fn from(p: Pool) -> Self {
281        CurrencyPair::from(p).into_any()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use rstest::rstest;
288
289    use super::DexType;
290
291    #[rstest]
292    fn test_dex_type_from_dex_name_valid() {
293        // Test some known DEX names
294        assert!(DexType::from_dex_name("UniswapV3").is_some());
295        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
296        assert!(DexType::from_dex_name("BalancerV2").is_some());
297        assert!(DexType::from_dex_name("CamelotV3").is_some());
298
299        // Verify specific DEX type
300        let uniswap_v3 = DexType::from_dex_name("UniswapV3").unwrap();
301        assert_eq!(uniswap_v3, DexType::UniswapV3);
302
303        // Verify compound names
304        let aerodrome_slipstream = DexType::from_dex_name("AerodromeSlipstream").unwrap();
305        assert_eq!(aerodrome_slipstream, DexType::AerodromeSlipstream);
306
307        // Verify specialized names
308        let fluid_dex = DexType::from_dex_name("FluidDEX").unwrap();
309        assert_eq!(fluid_dex, DexType::FluidDEX);
310    }
311
312    #[rstest]
313    fn test_dex_type_from_dex_name_invalid() {
314        // Test unknown DEX names
315        assert!(DexType::from_dex_name("InvalidDEX").is_none());
316        assert!(DexType::from_dex_name("").is_none());
317        assert!(DexType::from_dex_name("NonExistentDEX").is_none());
318    }
319
320    #[rstest]
321    fn test_dex_type_from_dex_name_case_sensitive() {
322        // Test case sensitivity - should be case sensitive
323        assert!(DexType::from_dex_name("UniswapV3").is_some());
324        assert!(DexType::from_dex_name("uniswapv3").is_none()); // lowercase
325        assert!(DexType::from_dex_name("UNISWAPV3").is_none()); // uppercase
326        assert!(DexType::from_dex_name("UniSwapV3").is_none()); // mixed case
327
328        assert!(DexType::from_dex_name("SushiSwapV2").is_some());
329        assert!(DexType::from_dex_name("sushiswapv2").is_none()); // lowercase
330    }
331
332    #[rstest]
333    fn test_dex_type_all_variants_mappable() {
334        // Test that all DEX variants can be mapped from their string representation
335        let all_dex_names = vec![
336            "AerodromeSlipstream",
337            "AerodromeV1",
338            "BalancerV2",
339            "BalancerV3",
340            "BaseSwapV2",
341            "BaseX",
342            "CamelotV3",
343            "CurveFinance",
344            "FluidDEX",
345            "MaverickV1",
346            "MaverickV2",
347            "PancakeSwapV3",
348            "SushiSwapV2",
349            "SushiSwapV3",
350            "UniswapV2",
351            "UniswapV3",
352            "UniswapV4",
353        ];
354
355        for dex_name in all_dex_names {
356            assert!(
357                DexType::from_dex_name(dex_name).is_some(),
358                "DEX name '{dex_name}' should be valid but was not found",
359            );
360        }
361    }
362
363    #[rstest]
364    fn test_dex_type_display() {
365        // Test that DexType variants display correctly (using strum::Display)
366        assert_eq!(DexType::UniswapV3.to_string(), "UniswapV3");
367        assert_eq!(DexType::SushiSwapV2.to_string(), "SushiSwapV2");
368        assert_eq!(
369            DexType::AerodromeSlipstream.to_string(),
370            "AerodromeSlipstream"
371        );
372        assert_eq!(DexType::FluidDEX.to_string(), "FluidDEX");
373    }
374}