1use std::{borrow::Cow, fmt::Display, str::FromStr, sync::Arc};
17
18use alloy_primitives::{Address, keccak256};
19use nautilus_core::{
20 correctness::{CorrectnessError, CorrectnessResultExt, FAILED},
21 hex,
22};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use strum::{Display, EnumIter, EnumString};
26
27use crate::{
28 defi::{amm::Pool, chain::Chain, validation::validate_address},
29 enums::CurrencyType,
30 instruments::{Instrument, any::InstrumentAny, currency_pair::CurrencyPair},
31 types::{currency::Currency, fixed::FIXED_PRECISION, price::Price, quantity::Quantity},
32};
33
34#[derive(
36 Debug,
37 Clone,
38 Copy,
39 Hash,
40 PartialEq,
41 Eq,
42 Serialize,
43 Deserialize,
44 strum::EnumString,
45 strum::Display,
46 strum::EnumIter,
47)]
48#[cfg_attr(
49 feature = "python",
50 pyo3::pyclass(
51 frozen,
52 eq,
53 eq_int,
54 module = "nautilus_trader.model",
55 from_py_object,
56 rename_all = "SCREAMING_SNAKE_CASE",
57 )
58)]
59#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
60#[non_exhaustive]
61pub enum AmmType {
62 CPAMM,
64 CLAMM,
66 CLAMEnhanced,
68 StableSwap,
70 WeightedPool,
72 ComposablePool,
74}
75
76#[derive(
78 Debug,
79 Clone,
80 Copy,
81 Hash,
82 PartialOrd,
83 PartialEq,
84 Ord,
85 Eq,
86 Display,
87 EnumIter,
88 EnumString,
89 Serialize,
90 Deserialize,
91)]
92#[cfg_attr(
93 feature = "python",
94 pyo3::pyclass(
95 frozen,
96 eq,
97 eq_int,
98 module = "nautilus_trader.model",
99 from_py_object,
100 rename_all = "SCREAMING_SNAKE_CASE",
101 )
102)]
103#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass_enum)]
104pub enum DexType {
105 AerodromeSlipstream,
106 AerodromeV1,
107 BalancerV2,
108 BalancerV3,
109 BaseSwapV2,
110 BaseX,
111 CamelotV3,
112 CurveFinance,
113 FluidDEX,
114 MaverickV1,
115 MaverickV2,
116 PancakeSwapV3,
117 SushiSwapV2,
118 SushiSwapV3,
119 UniswapV2,
120 UniswapV3,
121 UniswapV4,
122}
123
124impl DexType {
125 #[must_use]
127 pub fn from_dex_name(dex_name: &str) -> Option<Self> {
128 Self::from_str(dex_name).ok()
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[cfg_attr(
135 feature = "python",
136 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
137)]
138#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
139pub struct Dex {
140 pub chain: Chain,
142 pub name: DexType,
144 pub factory: Address,
146 pub factory_creation_block: u64,
148 pub pool_created_event: Cow<'static, str>,
150 pub initialize_event: Option<Cow<'static, str>>,
152 pub swap_created_event: Cow<'static, str>,
154 pub mint_created_event: Cow<'static, str>,
156 pub burn_created_event: Cow<'static, str>,
158 pub collect_created_event: Cow<'static, str>,
160 pub flash_created_event: Option<Cow<'static, str>>,
162 pub fee_protocol_update_event: Option<Cow<'static, str>>,
164 pub fee_protocol_collect_event: Option<Cow<'static, str>>,
166 pub amm_type: AmmType,
168 #[allow(dead_code)]
170 pairs: Vec<Pool>,
171}
172
173pub type SharedDex = Arc<Dex>;
175
176impl Dex {
177 #[must_use]
183 #[expect(clippy::too_many_arguments)]
184 pub fn new(
185 chain: Chain,
186 name: DexType,
187 factory: &str,
188 factory_creation_block: u64,
189 amm_type: AmmType,
190 pool_created_event: &str,
191 swap_event: &str,
192 mint_event: &str,
193 burn_event: &str,
194 collect_event: &str,
195 ) -> Self {
196 let encoded_pool_created_event =
197 hex::encode_prefixed(keccak256(pool_created_event.as_bytes()));
198 let encoded_swap_event = hex::encode_prefixed(keccak256(swap_event.as_bytes()));
199 let encoded_mint_event = hex::encode_prefixed(keccak256(mint_event.as_bytes()));
200 let encoded_burn_event = hex::encode_prefixed(keccak256(burn_event.as_bytes()));
201 let encoded_collect_event = hex::encode_prefixed(keccak256(collect_event.as_bytes()));
202 let factory_address = match validate_address(factory) {
203 Ok(address) => address,
204 Err(e) => panic!(
205 "Invalid factory address for DEX {name} on chain {chain} for factory address {factory}: {e}"
206 ),
207 };
208 Self {
209 chain,
210 name,
211 factory: factory_address,
212 factory_creation_block,
213 pool_created_event: encoded_pool_created_event.into(),
214 initialize_event: None,
215 swap_created_event: encoded_swap_event.into(),
216 mint_created_event: encoded_mint_event.into(),
217 burn_created_event: encoded_burn_event.into(),
218 collect_created_event: encoded_collect_event.into(),
219 flash_created_event: None,
220 fee_protocol_update_event: None,
221 fee_protocol_collect_event: None,
222 amm_type,
223 pairs: vec![],
224 }
225 }
226
227 #[must_use]
229 pub fn id(&self) -> String {
230 format!("{}:{}", self.chain.name, self.name)
231 }
232
233 pub fn set_initialize_event(&mut self, event: &str) {
235 self.initialize_event = Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
236 }
237
238 pub fn set_flash_event(&mut self, event: &str) {
240 self.flash_created_event = Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
241 }
242
243 pub fn set_fee_protocol_update_event(&mut self, event: &str) {
245 self.fee_protocol_update_event =
246 Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
247 }
248
249 pub fn set_fee_protocol_collect_event(&mut self, event: &str) {
251 self.fee_protocol_collect_event =
252 Some(hex::encode_prefixed(keccak256(event.as_bytes())).into());
253 }
254}
255
256impl Display for Dex {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 write!(f, "Dex(chain={}, name={})", self.chain, self.name)
259 }
260}
261
262impl TryFrom<&Pool> for CurrencyPair {
263 type Error = CorrectnessError;
264
265 fn try_from(p: &Pool) -> Result<Self, Self::Error> {
266 let size_precision = p.token0.decimals.min(FIXED_PRECISION);
267 let price_precision = p.token1.decimals.min(FIXED_PRECISION);
268
269 let price_increment =
270 Price::from_mantissa_exponent(1, -price_precision.cast_signed(), price_precision);
271 let size_increment =
272 Quantity::from_mantissa_exponent(1, -size_precision.cast_signed(), size_precision);
273 let base_currency = Currency::new_checked(
274 p.token0.symbol.as_str(),
275 size_precision,
276 0,
277 p.token0.name.as_str(),
278 CurrencyType::Crypto,
279 )?;
280 let quote_currency = Currency::new_checked(
281 p.token1.symbol.as_str(),
282 price_precision,
283 0,
284 p.token1.name.as_str(),
285 CurrencyType::Crypto,
286 )?;
287 let taker_fee = p.fee.map(|fee| Decimal::new(i64::from(fee), 6));
288
289 let pair = Self::builder()
290 .instrument_id(p.instrument_id)
291 .raw_symbol(p.instrument_id.symbol)
292 .base_currency(base_currency)
293 .quote_currency(quote_currency)
294 .price_precision(price_precision)
295 .size_precision(size_precision)
296 .price_increment(price_increment)
297 .size_increment(size_increment)
298 .maybe_taker_fee(taker_fee)
299 .ts_event(p.ts_event)
300 .ts_init(p.ts_init)
301 .build()?;
302
303 for currency in [base_currency, quote_currency] {
304 if let Err(e) = Currency::register(currency, false) {
305 log::error!(
306 "Failed to register DeFi token currency '{}': {e}",
307 currency.code
308 );
309 }
310 }
311
312 Ok(pair)
313 }
314}
315
316impl From<Pool> for CurrencyPair {
317 fn from(p: Pool) -> Self {
318 Self::try_from(&p).expect_display(FAILED)
319 }
320}
321
322impl From<Pool> for InstrumentAny {
323 fn from(p: Pool) -> Self {
324 CurrencyPair::from(p).into_any()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use nautilus_core::correctness::CorrectnessError;
331 use rstest::rstest;
332 use rust_decimal::Decimal;
333
334 use super::{CurrencyPair, DexType};
335 use crate::{
336 defi::{SharedPool, stubs::rain_pool},
337 enums::CurrencyType,
338 types::{currency::Currency, fixed::FIXED_PRECISION},
339 };
340
341 #[rstest]
342 fn test_dex_type_from_dex_name_valid() {
343 assert!(DexType::from_dex_name("UniswapV3").is_some());
345 assert!(DexType::from_dex_name("SushiSwapV2").is_some());
346 assert!(DexType::from_dex_name("BalancerV2").is_some());
347 assert!(DexType::from_dex_name("CamelotV3").is_some());
348
349 let uniswap_v3 = DexType::from_dex_name("UniswapV3").unwrap();
351 assert_eq!(uniswap_v3, DexType::UniswapV3);
352
353 let aerodrome_slipstream = DexType::from_dex_name("AerodromeSlipstream").unwrap();
355 assert_eq!(aerodrome_slipstream, DexType::AerodromeSlipstream);
356
357 let fluid_dex = DexType::from_dex_name("FluidDEX").unwrap();
359 assert_eq!(fluid_dex, DexType::FluidDEX);
360 }
361
362 #[rstest]
363 fn test_dex_type_from_dex_name_invalid() {
364 assert!(DexType::from_dex_name("InvalidDEX").is_none());
366 assert!(DexType::from_dex_name("").is_none());
367 assert!(DexType::from_dex_name("NonExistentDEX").is_none());
368 }
369
370 #[rstest]
371 fn test_dex_type_from_dex_name_case_sensitive() {
372 assert!(DexType::from_dex_name("UniswapV3").is_some());
374 assert!(DexType::from_dex_name("uniswapv3").is_none()); assert!(DexType::from_dex_name("UNISWAPV3").is_none()); assert!(DexType::from_dex_name("UniSwapV3").is_none()); assert!(DexType::from_dex_name("SushiSwapV2").is_some());
379 assert!(DexType::from_dex_name("sushiswapv2").is_none()); }
381
382 #[rstest]
383 fn test_dex_type_all_variants_mappable() {
384 let all_dex_names = vec![
386 "AerodromeSlipstream",
387 "AerodromeV1",
388 "BalancerV2",
389 "BalancerV3",
390 "BaseSwapV2",
391 "BaseX",
392 "CamelotV3",
393 "CurveFinance",
394 "FluidDEX",
395 "MaverickV1",
396 "MaverickV2",
397 "PancakeSwapV3",
398 "SushiSwapV2",
399 "SushiSwapV3",
400 "UniswapV2",
401 "UniswapV3",
402 "UniswapV4",
403 ];
404
405 for dex_name in all_dex_names {
406 assert!(
407 DexType::from_dex_name(dex_name).is_some(),
408 "DEX name '{dex_name}' should be valid but was not found",
409 );
410 }
411 }
412
413 #[rstest]
414 fn test_dex_type_display() {
415 assert_eq!(DexType::UniswapV3.to_string(), "UniswapV3");
417 assert_eq!(DexType::SushiSwapV2.to_string(), "SushiSwapV2");
418 assert_eq!(
419 DexType::AerodromeSlipstream.to_string(),
420 "AerodromeSlipstream"
421 );
422 assert_eq!(DexType::FluidDEX.to_string(), "FluidDEX");
423 }
424
425 #[rstest]
426 #[case(0, 6, 0, 6)]
427 #[case(6, FIXED_PRECISION, 6, FIXED_PRECISION)]
428 #[case(FIXED_PRECISION, 0, FIXED_PRECISION, 0)]
429 #[case(
430 FIXED_PRECISION + 1,
431 FIXED_PRECISION + 2,
432 FIXED_PRECISION,
433 FIXED_PRECISION
434 )]
435 fn test_pool_to_currency_pair_constructs_exact_increments(
436 #[case] size_precision: u8,
437 #[case] price_precision: u8,
438 #[case] expected_size_precision: u8,
439 #[case] expected_price_precision: u8,
440 rain_pool: SharedPool,
441 ) {
442 let mut pool = (*rain_pool).clone();
443 pool.token0.symbol = "BTC".to_string();
444 pool.token1.symbol = "USDC".to_string();
445 pool.token0.decimals = size_precision;
446 pool.token1.decimals = price_precision;
447
448 let expected_id = pool.instrument_id;
449 let expected_taker_fee = pool.fee.map(|fee| Decimal::new(i64::from(fee), 6));
450 let expected_ts_event = pool.ts_event;
451 let expected_ts_init = pool.ts_init;
452 let pair = CurrencyPair::from(pool);
453 let price_scale_exponent = u32::from(FIXED_PRECISION - expected_price_precision);
454 let size_scale_exponent = u32::from(FIXED_PRECISION - expected_size_precision);
455
456 assert_eq!(pair.id, expected_id);
457 assert_eq!(pair.raw_symbol, expected_id.symbol);
458 assert_eq!(pair.base_currency.code.as_str(), "BTC");
459 assert_eq!(pair.base_currency.precision, expected_size_precision);
460 assert_eq!(pair.quote_currency.code.as_str(), "USDC");
461 assert_eq!(pair.quote_currency.precision, expected_price_precision);
462 assert_eq!(pair.price_precision, expected_price_precision);
463 assert_eq!(pair.size_precision, expected_size_precision);
464 assert_eq!(pair.price_increment.raw, 10_i128.pow(price_scale_exponent));
465 assert_eq!(pair.price_increment.precision, expected_price_precision);
466 assert_eq!(pair.size_increment.raw, 10_u128.pow(size_scale_exponent));
467 assert_eq!(pair.size_increment.precision, expected_size_precision);
468 assert_eq!(pair.maker_fee, Decimal::ZERO);
469 assert_eq!(pair.taker_fee, expected_taker_fee.unwrap());
470 assert_eq!(pair.ts_event, expected_ts_event);
471 assert_eq!(pair.ts_init, expected_ts_init);
472 }
473
474 #[rstest]
475 fn test_pool_to_currency_pair_registers_token_currencies(rain_pool: SharedPool) {
476 let mut pool = (*rain_pool).clone();
477 pool.token0.symbol = "ENG444BASE".to_string();
478 pool.token0.name = "ENG-444 Base Token".to_string();
479 pool.token0.decimals = 8;
480 pool.token1.symbol = "ENG444QUOTE".to_string();
481 pool.token1.name = "ENG-444 Quote Token".to_string();
482 pool.token1.decimals = 6;
483
484 let _ = CurrencyPair::from(pool);
485
486 let base = Currency::try_from_str("ENG444BASE").unwrap();
487 let quote = Currency::try_from_str("ENG444QUOTE").unwrap();
488 assert_eq!(base.code.as_str(), "ENG444BASE");
489 assert_eq!(base.precision, 8);
490 assert_eq!(base.iso4217, 0);
491 assert_eq!(base.name.as_str(), "ENG-444 Base Token");
492 assert_eq!(base.currency_type, CurrencyType::Crypto);
493 assert_eq!(quote.code.as_str(), "ENG444QUOTE");
494 assert_eq!(quote.precision, 6);
495 assert_eq!(quote.iso4217, 0);
496 assert_eq!(quote.name.as_str(), "ENG-444 Quote Token");
497 assert_eq!(quote.currency_type, CurrencyType::Crypto);
498 }
499
500 #[rstest]
501 fn test_pool_to_currency_pair_rejects_invalid_token_metadata(rain_pool: SharedPool) {
502 let mut missing_symbol = (*rain_pool).clone();
503 missing_symbol.token0.symbol.clear();
504 let mut blank_symbol = (*rain_pool).clone();
505 blank_symbol.token0.symbol = " ".to_string();
506 let mut missing_name = (*rain_pool).clone();
507 missing_name.token0.name.clear();
508
509 let missing_symbol_result = CurrencyPair::try_from(&missing_symbol);
510 let blank_symbol_result = CurrencyPair::try_from(&blank_symbol);
511 let missing_name_result = CurrencyPair::try_from(&missing_name);
512
513 assert_eq!(
514 missing_symbol_result.unwrap_err(),
515 CorrectnessError::EmptyString {
516 param: "code".to_string(),
517 }
518 );
519 assert_eq!(
520 blank_symbol_result.unwrap_err(),
521 CorrectnessError::WhitespaceString {
522 param: "code".to_string(),
523 }
524 );
525 assert_eq!(
526 missing_name_result.unwrap_err(),
527 CorrectnessError::EmptyString {
528 param: "name".to_string(),
529 }
530 );
531 }
532}