nautilus_blockchain/exchanges/
mod.rs1use nautilus_model::defi::{Blockchain, Chain, DexType};
17
18use crate::exchanges::{
19 arbitrum::ARBITRUM_DEX_EXTENDED_MAP, base::BASE_DEX_EXTENDED_MAP, bsc::BSC_DEX_EXTENDED_MAP,
20 ethereum::ETHEREUM_DEX_EXTENDED_MAP, extended::DexExtended,
21};
22
23pub mod arbitrum;
24pub mod base;
25pub mod bsc;
26pub mod ethereum;
27pub mod extended;
28pub mod parsing;
29
30#[must_use]
32pub fn get_dex_extended(
33 blockchain: Blockchain,
34 dex_type: &DexType,
35) -> Option<&'static DexExtended> {
36 match blockchain {
37 Blockchain::Ethereum => ETHEREUM_DEX_EXTENDED_MAP.get(dex_type).copied(),
38 Blockchain::Base => BASE_DEX_EXTENDED_MAP.get(dex_type).copied(),
39 Blockchain::Arbitrum => ARBITRUM_DEX_EXTENDED_MAP.get(dex_type).copied(),
40 Blockchain::Bsc => BSC_DEX_EXTENDED_MAP.get(dex_type).copied(),
41 _ => None,
42 }
43}
44
45#[must_use]
47pub fn get_supported_dexes_for_chain(blockchain: Blockchain) -> Vec<String> {
48 let dex_types: Vec<DexType> = match blockchain {
49 Blockchain::Ethereum => ETHEREUM_DEX_EXTENDED_MAP.keys().copied().collect(),
50 Blockchain::Base => BASE_DEX_EXTENDED_MAP.keys().copied().collect(),
51 Blockchain::Arbitrum => ARBITRUM_DEX_EXTENDED_MAP.keys().copied().collect(),
52 Blockchain::Bsc => BSC_DEX_EXTENDED_MAP.keys().copied().collect(),
53 _ => vec![],
54 };
55
56 dex_types
57 .into_iter()
58 .map(|dex_type| format!("{dex_type}"))
59 .collect()
60}
61
62pub const DEX_SUPPORTED_CHAINS: [Blockchain; 4] = [
67 Blockchain::Ethereum,
68 Blockchain::Base,
69 Blockchain::Arbitrum,
70 Blockchain::Bsc,
71];
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct DexCapability {
76 pub dex_type: DexType,
78 pub discovery: bool,
80 pub snapshots: bool,
82 pub replay_ready: bool,
84}
85
86#[must_use]
90pub fn dex_capabilities_for_chain(blockchain: Blockchain) -> Vec<DexCapability> {
91 let mut dex_types: Vec<DexType> = match blockchain {
92 Blockchain::Ethereum => ETHEREUM_DEX_EXTENDED_MAP.keys().copied().collect(),
93 Blockchain::Base => BASE_DEX_EXTENDED_MAP.keys().copied().collect(),
94 Blockchain::Arbitrum => ARBITRUM_DEX_EXTENDED_MAP.keys().copied().collect(),
95 Blockchain::Bsc => BSC_DEX_EXTENDED_MAP.keys().copied().collect(),
96 _ => return Vec::new(),
97 };
98 dex_types.sort_by_key(ToString::to_string);
99
100 dex_types
101 .into_iter()
102 .filter_map(|dex_type| {
103 let dex = get_dex_extended(blockchain, &dex_type)?;
104 Some(DexCapability {
105 dex_type,
106 discovery: dex.supports_pool_discovery(),
107 snapshots: dex.missing_pool_analysis_parsers().is_empty(),
108 replay_ready: dex.supports_fee_protocol_replay(),
109 })
110 })
111 .collect()
112}
113
114pub fn find_dex_type_case_insensitive(dex_name: &str, chain: &Chain) -> Option<DexType> {
116 let supported_dexes = get_supported_dexes_for_chain(chain.name);
117
118 if let Some(dex_type) = DexType::from_dex_name(dex_name) {
120 return Some(dex_type);
121 }
122
123 for supported_dex in supported_dexes {
125 if supported_dex.to_lowercase() == dex_name.to_lowercase() {
126 return DexType::from_dex_name(&supported_dex);
127 }
128 }
129
130 None
131}
132
133#[cfg(test)]
134mod tests {
135 use alloy::primitives::keccak256;
136 use nautilus_core::hex;
137 use nautilus_model::defi::pool_analysis::PoolEventKind;
138 use rstest::rstest;
139
140 use super::*;
141
142 fn empty_signature_hash() -> String {
145 hex::encode_prefixed(keccak256("".as_bytes()))
146 }
147
148 const KNOWN_PARSER_GAPS: &[(Blockchain, DexType, &str)] = &[];
152
153 fn is_known_gap(blockchain: Blockchain, dex_type: DexType, event: &str) -> bool {
154 KNOWN_PARSER_GAPS
155 .iter()
156 .any(|(b, d, e)| *b == blockchain && *d == dex_type && *e == event)
157 }
158
159 fn collect_parity_gaps(blockchain: Blockchain, dex_type: DexType, gaps: &mut Vec<String>) {
160 let dex_extended = get_dex_extended(blockchain, &dex_type).unwrap_or_else(|| {
161 panic!("{blockchain:?}:{dex_type:?} should be registered in the DEX map")
162 });
163 let empty = empty_signature_hash();
164 let dex = &dex_extended.dex;
165
166 let mut record = |event: &str, has_parser: bool| {
167 if !has_parser && !is_known_gap(blockchain, dex_type, event) {
168 gaps.push(format!(
169 "{blockchain:?}:{dex_type:?} advertises {event} but has no HyperSync parser"
170 ));
171 }
172 };
173
174 if dex.pool_created_event.as_ref() != empty {
175 record(
176 "PoolCreated",
177 dex_extended.parse_pool_created_event_hypersync_fn.is_some(),
178 );
179 }
180
181 if dex.swap_created_event.as_ref() != empty {
182 record("Swap", dex_extended.parse_swap_event_hypersync_fn.is_some());
183 }
184
185 if dex.mint_created_event.as_ref() != empty {
186 record("Mint", dex_extended.parse_mint_event_hypersync_fn.is_some());
187 }
188
189 if dex.burn_created_event.as_ref() != empty {
190 record("Burn", dex_extended.parse_burn_event_hypersync_fn.is_some());
191 }
192
193 if dex.collect_created_event.as_ref() != empty {
194 record(
195 "Collect",
196 dex_extended.parse_collect_event_hypersync_fn.is_some(),
197 );
198 }
199
200 if dex.initialize_event.is_some() {
201 record(
202 "Initialize",
203 dex_extended.parse_initialize_event_hypersync_fn.is_some(),
204 );
205 }
206
207 if dex.flash_created_event.is_some() {
208 record(
209 "Flash",
210 dex_extended.parse_flash_event_hypersync_fn.is_some(),
211 );
212 }
213
214 if dex.fee_protocol_update_event.is_some() {
215 record(
216 "SetFeeProtocol",
217 dex_extended
218 .parse_fee_protocol_update_event_hypersync_fn
219 .is_some(),
220 );
221 }
222
223 if dex.fee_protocol_collect_event.is_some() {
224 record(
225 "CollectProtocol",
226 dex_extended
227 .parse_fee_protocol_collect_event_hypersync_fn
228 .is_some(),
229 );
230 }
231 }
232
233 #[rstest]
234 #[case(Blockchain::Ethereum)]
235 #[case(Blockchain::Base)]
236 #[case(Blockchain::Arbitrum)]
237 #[case(Blockchain::Bsc)]
238 fn test_dex_signature_parser_parity_for_chain(#[case] blockchain: Blockchain) {
239 let dex_types: Vec<DexType> = match blockchain {
240 Blockchain::Ethereum => ETHEREUM_DEX_EXTENDED_MAP.keys().copied().collect(),
241 Blockchain::Base => BASE_DEX_EXTENDED_MAP.keys().copied().collect(),
242 Blockchain::Arbitrum => ARBITRUM_DEX_EXTENDED_MAP.keys().copied().collect(),
243 Blockchain::Bsc => BSC_DEX_EXTENDED_MAP.keys().copied().collect(),
244 _ => panic!("unsupported chain in test"),
245 };
246 assert!(
247 !dex_types.is_empty(),
248 "{blockchain:?} should register at least one DEX"
249 );
250
251 let mut gaps = Vec::new();
252 for dex_type in dex_types {
253 collect_parity_gaps(blockchain, dex_type, &mut gaps);
254 }
255 assert!(
256 gaps.is_empty(),
257 "DEX parser parity violations for {blockchain:?}:\n{}",
258 gaps.join("\n")
259 );
260 }
261
262 #[rstest]
263 #[case(Blockchain::Bsc, DexType::UniswapV3)]
264 #[case(Blockchain::Bsc, DexType::PancakeSwapV3)]
265 fn test_bsc_dispatch_returns_registered_dex(
266 #[case] blockchain: Blockchain,
267 #[case] dex_type: DexType,
268 ) {
269 let dex_extended = get_dex_extended(blockchain, &dex_type)
270 .expect("BSC dispatch should return the registered DEX");
271 assert_eq!(dex_extended.dex.chain.name, blockchain);
272 assert_eq!(dex_extended.dex.name, dex_type);
273 }
274
275 #[rstest]
276 #[case(Blockchain::Bsc)]
277 #[case(Blockchain::Base)]
278 #[case(Blockchain::Arbitrum)]
279 #[case(Blockchain::Ethereum)]
280 fn test_pancakeswap_v3_supports_pool_analysis(#[case] blockchain: Blockchain) {
281 let dex_extended = get_dex_extended(blockchain, &DexType::PancakeSwapV3)
282 .expect("PancakeSwapV3 should be registered");
283 assert!(dex_extended.supports_pool_discovery());
284 assert!(
285 dex_extended.missing_pool_analysis_parsers().is_empty(),
286 "PancakeSwapV3 on {blockchain:?} is missing analysis parsers: {:?}",
287 dex_extended.missing_pool_analysis_parsers()
288 );
289 }
290
291 #[rstest]
292 #[case(Blockchain::Ethereum, DexType::UniswapV3, true, true, true)]
293 #[case(Blockchain::Base, DexType::UniswapV3, true, true, true)]
294 #[case(Blockchain::Bsc, DexType::PancakeSwapV3, true, true, false)]
295 #[case(Blockchain::Base, DexType::AerodromeSlipstream, false, true, false)]
296 #[case(Blockchain::Ethereum, DexType::UniswapV2, true, false, false)]
297 #[case(Blockchain::Arbitrum, DexType::SushiSwapV2, false, false, false)]
298 fn test_dex_capability_tiers(
299 #[case] blockchain: Blockchain,
300 #[case] dex_type: DexType,
301 #[case] discovery: bool,
302 #[case] snapshots: bool,
303 #[case] replay_ready: bool,
304 ) {
305 let capability = dex_capabilities_for_chain(blockchain)
306 .into_iter()
307 .find(|c| c.dex_type == dex_type)
308 .unwrap_or_else(|| panic!("{dex_type:?} should be registered on {blockchain:?}"));
309
310 assert_eq!(capability.discovery, discovery);
311 assert_eq!(capability.snapshots, snapshots);
312 assert_eq!(capability.replay_ready, replay_ready);
313 }
314
315 #[rstest]
316 fn test_dex_capabilities_empty_for_chain_without_map() {
317 assert!(dex_capabilities_for_chain(Blockchain::Polygon).is_empty());
318 }
319
320 #[rstest]
321 fn test_dex_capabilities_sorted_by_name() {
322 let names: Vec<String> = dex_capabilities_for_chain(Blockchain::Arbitrum)
323 .iter()
324 .map(|c| c.dex_type.to_string())
325 .collect();
326 let mut sorted = names.clone();
327 sorted.sort();
328
329 assert_eq!(names, sorted);
330 }
331
332 #[rstest]
333 fn test_dex_without_parsers_reports_unsupported() {
334 let dex_extended = get_dex_extended(Blockchain::Arbitrum, &DexType::SushiSwapV3)
336 .expect("SushiSwapV3 should be registered on Arbitrum");
337 assert!(!dex_extended.supports_pool_discovery());
338 assert_eq!(
339 dex_extended.missing_pool_analysis_parsers(),
340 vec![
341 PoolEventKind::Initialize,
342 PoolEventKind::Swap,
343 PoolEventKind::Mint,
344 PoolEventKind::Burn,
345 PoolEventKind::Collect,
346 ]
347 );
348 }
349}