nautilus_model/defi/
dex.rs

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