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