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::new_checked(
290 p.instrument_id,
291 p.instrument_id.symbol,
292 base_currency,
293 quote_currency,
294 price_precision,
295 size_precision,
296 price_increment,
297 size_increment,
298 None, None, None, None, None, None, None, None, None, None, None, taker_fee,
310 None, None, p.ts_event,
313 p.ts_init,
314 )?;
315
316 for currency in [base_currency, quote_currency] {
317 if let Err(e) = Currency::register(currency, false) {
318 log::error!(
319 "Failed to register DeFi token currency '{}': {e}",
320 currency.code
321 );
322 }
323 }
324
325 Ok(pair)
326 }
327}
328
329impl From<Pool> for CurrencyPair {
330 fn from(p: Pool) -> Self {
331 Self::try_from(&p).expect_display(FAILED)
332 }
333}
334
335impl From<Pool> for InstrumentAny {
336 fn from(p: Pool) -> Self {
337 CurrencyPair::from(p).into_any()
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use nautilus_core::correctness::CorrectnessError;
344 use rstest::rstest;
345 use rust_decimal::Decimal;
346
347 use super::{CurrencyPair, DexType};
348 use crate::{
349 defi::{SharedPool, stubs::rain_pool},
350 enums::CurrencyType,
351 types::{currency::Currency, fixed::FIXED_PRECISION},
352 };
353
354 #[rstest]
355 fn test_dex_type_from_dex_name_valid() {
356 assert!(DexType::from_dex_name("UniswapV3").is_some());
358 assert!(DexType::from_dex_name("SushiSwapV2").is_some());
359 assert!(DexType::from_dex_name("BalancerV2").is_some());
360 assert!(DexType::from_dex_name("CamelotV3").is_some());
361
362 let uniswap_v3 = DexType::from_dex_name("UniswapV3").unwrap();
364 assert_eq!(uniswap_v3, DexType::UniswapV3);
365
366 let aerodrome_slipstream = DexType::from_dex_name("AerodromeSlipstream").unwrap();
368 assert_eq!(aerodrome_slipstream, DexType::AerodromeSlipstream);
369
370 let fluid_dex = DexType::from_dex_name("FluidDEX").unwrap();
372 assert_eq!(fluid_dex, DexType::FluidDEX);
373 }
374
375 #[rstest]
376 fn test_dex_type_from_dex_name_invalid() {
377 assert!(DexType::from_dex_name("InvalidDEX").is_none());
379 assert!(DexType::from_dex_name("").is_none());
380 assert!(DexType::from_dex_name("NonExistentDEX").is_none());
381 }
382
383 #[rstest]
384 fn test_dex_type_from_dex_name_case_sensitive() {
385 assert!(DexType::from_dex_name("UniswapV3").is_some());
387 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());
392 assert!(DexType::from_dex_name("sushiswapv2").is_none()); }
394
395 #[rstest]
396 fn test_dex_type_all_variants_mappable() {
397 let all_dex_names = vec![
399 "AerodromeSlipstream",
400 "AerodromeV1",
401 "BalancerV2",
402 "BalancerV3",
403 "BaseSwapV2",
404 "BaseX",
405 "CamelotV3",
406 "CurveFinance",
407 "FluidDEX",
408 "MaverickV1",
409 "MaverickV2",
410 "PancakeSwapV3",
411 "SushiSwapV2",
412 "SushiSwapV3",
413 "UniswapV2",
414 "UniswapV3",
415 "UniswapV4",
416 ];
417
418 for dex_name in all_dex_names {
419 assert!(
420 DexType::from_dex_name(dex_name).is_some(),
421 "DEX name '{dex_name}' should be valid but was not found",
422 );
423 }
424 }
425
426 #[rstest]
427 fn test_dex_type_display() {
428 assert_eq!(DexType::UniswapV3.to_string(), "UniswapV3");
430 assert_eq!(DexType::SushiSwapV2.to_string(), "SushiSwapV2");
431 assert_eq!(
432 DexType::AerodromeSlipstream.to_string(),
433 "AerodromeSlipstream"
434 );
435 assert_eq!(DexType::FluidDEX.to_string(), "FluidDEX");
436 }
437
438 #[rstest]
439 #[case(0, 6, 0, 6)]
440 #[case(6, FIXED_PRECISION, 6, FIXED_PRECISION)]
441 #[case(FIXED_PRECISION, 0, FIXED_PRECISION, 0)]
442 #[case(
443 FIXED_PRECISION + 1,
444 FIXED_PRECISION + 2,
445 FIXED_PRECISION,
446 FIXED_PRECISION
447 )]
448 fn test_pool_to_currency_pair_constructs_exact_increments(
449 #[case] size_precision: u8,
450 #[case] price_precision: u8,
451 #[case] expected_size_precision: u8,
452 #[case] expected_price_precision: u8,
453 rain_pool: SharedPool,
454 ) {
455 let mut pool = (*rain_pool).clone();
456 pool.token0.symbol = "BTC".to_string();
457 pool.token1.symbol = "USDC".to_string();
458 pool.token0.decimals = size_precision;
459 pool.token1.decimals = price_precision;
460
461 let expected_id = pool.instrument_id;
462 let expected_taker_fee = pool.fee.map(|fee| Decimal::new(i64::from(fee), 6));
463 let expected_ts_event = pool.ts_event;
464 let expected_ts_init = pool.ts_init;
465 let pair = CurrencyPair::from(pool);
466 let price_scale_exponent = u32::from(FIXED_PRECISION - expected_price_precision);
467 let size_scale_exponent = u32::from(FIXED_PRECISION - expected_size_precision);
468
469 assert_eq!(pair.id, expected_id);
470 assert_eq!(pair.raw_symbol, expected_id.symbol);
471 assert_eq!(pair.base_currency.code.as_str(), "BTC");
472 assert_eq!(pair.base_currency.precision, expected_size_precision);
473 assert_eq!(pair.quote_currency.code.as_str(), "USDC");
474 assert_eq!(pair.quote_currency.precision, expected_price_precision);
475 assert_eq!(pair.price_precision, expected_price_precision);
476 assert_eq!(pair.size_precision, expected_size_precision);
477 assert_eq!(pair.price_increment.raw, 10_i128.pow(price_scale_exponent));
478 assert_eq!(pair.price_increment.precision, expected_price_precision);
479 assert_eq!(pair.size_increment.raw, 10_u128.pow(size_scale_exponent));
480 assert_eq!(pair.size_increment.precision, expected_size_precision);
481 assert_eq!(pair.maker_fee, Decimal::ZERO);
482 assert_eq!(pair.taker_fee, expected_taker_fee.unwrap());
483 assert_eq!(pair.ts_event, expected_ts_event);
484 assert_eq!(pair.ts_init, expected_ts_init);
485 }
486
487 #[rstest]
488 fn test_pool_to_currency_pair_registers_token_currencies(rain_pool: SharedPool) {
489 let mut pool = (*rain_pool).clone();
490 pool.token0.symbol = "ENG444BASE".to_string();
491 pool.token0.name = "ENG-444 Base Token".to_string();
492 pool.token0.decimals = 8;
493 pool.token1.symbol = "ENG444QUOTE".to_string();
494 pool.token1.name = "ENG-444 Quote Token".to_string();
495 pool.token1.decimals = 6;
496
497 let _ = CurrencyPair::from(pool);
498
499 let base = Currency::try_from_str("ENG444BASE").unwrap();
500 let quote = Currency::try_from_str("ENG444QUOTE").unwrap();
501 assert_eq!(base.code.as_str(), "ENG444BASE");
502 assert_eq!(base.precision, 8);
503 assert_eq!(base.iso4217, 0);
504 assert_eq!(base.name.as_str(), "ENG-444 Base Token");
505 assert_eq!(base.currency_type, CurrencyType::Crypto);
506 assert_eq!(quote.code.as_str(), "ENG444QUOTE");
507 assert_eq!(quote.precision, 6);
508 assert_eq!(quote.iso4217, 0);
509 assert_eq!(quote.name.as_str(), "ENG-444 Quote Token");
510 assert_eq!(quote.currency_type, CurrencyType::Crypto);
511 }
512
513 #[rstest]
514 fn test_pool_to_currency_pair_rejects_invalid_token_metadata(rain_pool: SharedPool) {
515 let mut missing_symbol = (*rain_pool).clone();
516 missing_symbol.token0.symbol.clear();
517 let mut blank_symbol = (*rain_pool).clone();
518 blank_symbol.token0.symbol = " ".to_string();
519 let mut missing_name = (*rain_pool).clone();
520 missing_name.token0.name.clear();
521
522 let missing_symbol_result = CurrencyPair::try_from(&missing_symbol);
523 let blank_symbol_result = CurrencyPair::try_from(&blank_symbol);
524 let missing_name_result = CurrencyPair::try_from(&missing_name);
525
526 assert_eq!(
527 missing_symbol_result.unwrap_err(),
528 CorrectnessError::EmptyString {
529 param: "code".to_string(),
530 }
531 );
532 assert_eq!(
533 blank_symbol_result.unwrap_err(),
534 CorrectnessError::WhitespaceString {
535 param: "code".to_string(),
536 }
537 );
538 assert_eq!(
539 missing_name_result.unwrap_err(),
540 CorrectnessError::EmptyString {
541 param: "name".to_string(),
542 }
543 );
544 }
545}