nautilus_blockchain/exchanges/ethereum/
uniswap_v3.rs1use std::sync::LazyLock;
17
18use alloy::{
19 primitives::{Address, Signed, U160, U256},
20 sol,
21 sol_types::SolType,
22};
23use hypersync_client::simple_types::Log;
24use nautilus_model::{
25 defi::{
26 chain::chains,
27 dex::{AmmType, Dex},
28 token::Token,
29 },
30 enums::OrderSide,
31 types::{Price, Quantity, fixed::FIXED_PRECISION},
32};
33
34use crate::{
35 events::{burn::BurnEvent, mint::MintEvent, pool_created::PoolCreatedEvent, swap::SwapEvent},
36 exchanges::extended::DexExtended,
37 hypersync::helpers::{
38 extract_block_number, extract_log_index, extract_transaction_hash,
39 extract_transaction_index, validate_event_signature_hash,
40 },
41 math::convert_i256_to_f64,
42};
43
44const POOL_CREATED_EVENT_SIGNATURE_HASH: &str =
45 "783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118";
46const SWAP_EVENT_SIGNATURE_HASH: &str =
47 "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
48const MINT_EVENT_SIGNATURE_HASH: &str =
49 "7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde";
50const BURN_EVENT_SIGNATURE_HASH: &str =
51 "0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c";
52
53pub static UNISWAP_V3: LazyLock<DexExtended> = LazyLock::new(|| {
55 let mut dex = DexExtended::new(Dex::new(
56 chains::ETHEREUM.clone(),
57 "Uniswap V3",
58 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
59 AmmType::CLAMM,
60 "PoolCreated(address,address,uint24,int24,address)",
61 "Swap(address,address,int256,int256,uint160,uint128,int24)",
62 "Mint(address,address,int24,int24,uint128,uint256,uint256)",
63 "Burn(address,int24,int24,uint128,uint256,uint256)",
64 ));
65 dex.set_pool_created_event_parsing(parse_pool_created_event);
66 dex.set_swap_event_parsing(parse_swap_event);
67 dex.set_convert_trade_data(convert_to_trade_data);
68 dex.set_mint_event_parsing(parse_mint_event);
69 dex.set_burn_event_parsing(parse_burn_event);
70 dex
71});
72
73fn parse_pool_created_event(log: Log) -> anyhow::Result<PoolCreatedEvent> {
74 validate_event_signature_hash("PoolCreatedEvent", POOL_CREATED_EVENT_SIGNATURE_HASH, &log)?;
75
76 let block_number = log
77 .block_number
78 .expect("Block number should be set in logs");
79
80 let token = if let Some(topic) = log.topics.get(1).and_then(|t| t.as_ref()) {
81 Address::from_slice(&topic.as_ref()[12..32])
83 } else {
84 anyhow::bail!("Missing token0 address in topic1 when parsing pool created event");
85 };
86
87 let token1 = if let Some(topic) = log.topics.get(2).and_then(|t| t.as_ref()) {
88 Address::from_slice(&topic.as_ref()[12..32])
89 } else {
90 anyhow::bail!("Missing token1 address in topic2 when parsing pool created event");
91 };
92
93 let fee = if let Some(topic) = log.topics.get(3).and_then(|t| t.as_ref()) {
94 U256::from_be_slice(topic.as_ref()).as_limbs()[0] as u32
95 } else {
96 anyhow::bail!("Missing fee in topic3 when parsing pool created event");
97 };
98
99 if let Some(data) = log.data {
100 let data_bytes = data.as_ref();
102
103 let tick_spacing_bytes: [u8; 32] = data_bytes[0..32].try_into()?;
105 let tick_spacing = u32::from_be_bytes(tick_spacing_bytes[28..32].try_into()?);
106
107 let pool_address_bytes: [u8; 32] = data_bytes[32..64].try_into()?;
109 let pool_address = Address::from_slice(&pool_address_bytes[12..32]);
110
111 Ok(PoolCreatedEvent::new(
112 block_number.into(),
113 token,
114 token1,
115 fee,
116 tick_spacing,
117 pool_address,
118 ))
119 } else {
120 Err(anyhow::anyhow!("Missing data in pool created event log"))
121 }
122}
123
124sol! {
128 struct SwapEventData {
129 int256 amount0;
130 int256 amount1;
131 uint160 sqrt_price_x96;
132 uint128 liquidity;
133 int24 tick;
134 }
135}
136
137fn parse_swap_event(log: Log) -> anyhow::Result<SwapEvent> {
138 validate_event_signature_hash("SwapEvent", SWAP_EVENT_SIGNATURE_HASH, &log)?;
139
140 let sender = match log.topics.get(1).and_then(|t| t.as_ref()) {
141 Some(topic) => Address::from_slice(&topic.as_ref()[12..32]),
142 None => anyhow::bail!("Missing sender address in topic1 when parsing swap event"),
143 };
144
145 let recipient = match log.topics.get(2).and_then(|t| t.as_ref()) {
146 Some(topic) => Address::from_slice(&topic.as_ref()[12..32]),
147 None => anyhow::bail!("Missing recipient address in topic2 when parsing swap event"),
148 };
149
150 if let Some(data) = &log.data {
151 let data_bytes = data.as_ref();
152
153 if data_bytes.len() < 5 * 32 {
155 anyhow::bail!("Swap event data is too short");
156 }
157
158 let decoded = match <SwapEventData as SolType>::abi_decode(data_bytes) {
160 Ok(decoded) => decoded,
161 Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
162 };
163 decoded.amount0;
164
165 Ok(SwapEvent::new(
166 extract_block_number(&log)?,
167 extract_transaction_hash(&log)?,
168 extract_transaction_index(&log)?,
169 extract_log_index(&log)?,
170 sender,
171 recipient,
172 decoded.amount0,
173 decoded.amount1,
174 decoded.sqrt_price_x96,
175 ))
176 } else {
177 Err(anyhow::anyhow!("Missing data in swap event log"))
178 }
179}
180
181fn calculate_price_from_sqrt_price(
183 sqrt_price_x96: U160,
184 token0_decimals: u8,
185 token1_decimals: u8,
186) -> f64 {
187 let sqrt_price = sqrt_price_x96 >> 96;
188 let price = sqrt_price * sqrt_price;
189 let price: f64 = U256::from(price)
190 .to_string()
191 .parse()
192 .expect("Failed to parse U256 to f64");
193 let token0_multiplier = 10u128.pow(u32::from(token0_decimals));
194 let token1_multiplier = 10u128.pow(u32::from(token1_decimals));
195 let factor = token1_multiplier as f64 / token0_multiplier as f64;
196 factor / price
197}
198
199fn convert_to_trade_data(
200 token0: &Token,
201 token1: &Token,
202 swap_event: &SwapEvent,
203) -> anyhow::Result<(OrderSide, Quantity, Price)> {
204 let price_f64 = calculate_price_from_sqrt_price(
205 swap_event.sqrt_price_x96,
206 token0.decimals,
207 token1.decimals,
208 );
209 let price = Price::from(format!(
210 "{:.precision$}",
211 price_f64,
212 precision = FIXED_PRECISION as usize
213 ));
214 let quantity_f64 = convert_i256_to_f64(swap_event.amount1, token1.decimals)?.abs();
215 let quantity = Quantity::from(format!(
216 "{:.precision$}",
217 quantity_f64,
218 precision = FIXED_PRECISION as usize
219 ));
220 let zero = Signed::<256, 4>::ZERO;
221 let side = if swap_event.amount1 > zero {
222 OrderSide::Sell
223 } else {
224 OrderSide::Buy
225 };
226 Ok((side, quantity, price))
227}
228
229sol! {
233 struct MintEventData {
234 address sender;
235 uint128 amount;
236 uint256 amount0;
237 uint256 amount1;
238 }
239}
240
241fn parse_mint_event(log: Log) -> anyhow::Result<MintEvent> {
242 validate_event_signature_hash("Mint", MINT_EVENT_SIGNATURE_HASH, &log)?;
243
244 let owner = match log.topics.get(1).and_then(|t| t.as_ref()) {
245 Some(topic) => Address::from_slice(&topic.as_ref()[12..32]),
246 None => anyhow::bail!("Missing owner address in topic1 when parsing mint event"),
247 };
248
249 let tick_lower = match log.topics.get(2).and_then(|t| t.as_ref()) {
251 Some(topic) => {
252 let tick_lower_bytes: [u8; 32] = topic.as_ref().try_into()?;
253 i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?)
254 }
255 None => anyhow::bail!("Missing tickLower in topic2 when parsing mint event"),
256 };
257
258 let tick_upper = match log.topics.get(3).and_then(|t| t.as_ref()) {
260 Some(topic) => {
261 let tick_upper_bytes: [u8; 32] = topic.as_ref().try_into()?;
262 i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?)
263 }
264 None => anyhow::bail!("Missing tickUpper in topic3 when parsing mint event"),
265 };
266
267 if let Some(data) = &log.data {
268 let data_bytes = data.as_ref();
269
270 if data_bytes.len() < 4 * 32 {
272 anyhow::bail!("Mint event data is too short");
273 }
274
275 let decoded = match <MintEventData as SolType>::abi_decode(data_bytes) {
277 Ok(decoded) => decoded,
278 Err(e) => anyhow::bail!("Failed to decode mint event data: {e}"),
279 };
280
281 Ok(MintEvent::new(
282 extract_block_number(&log)?,
283 extract_transaction_hash(&log)?,
284 extract_transaction_index(&log)?,
285 extract_log_index(&log)?,
286 decoded.sender,
287 owner,
288 tick_lower,
289 tick_upper,
290 decoded.amount,
291 decoded.amount0,
292 decoded.amount1,
293 ))
294 } else {
295 Err(anyhow::anyhow!("Missing data in mint event log"))
296 }
297}
298
299sol! {
303 struct BurnEventData {
304 uint128 amount;
305 uint256 amount0;
306 uint256 amount1;
307 }
308}
309
310fn parse_burn_event(log: Log) -> anyhow::Result<BurnEvent> {
311 validate_event_signature_hash("Burn", BURN_EVENT_SIGNATURE_HASH, &log)?;
312
313 let owner = match log.topics.get(1).and_then(|t| t.as_ref()) {
314 Some(topic) => Address::from_slice(&topic.as_ref()[12..32]),
315 None => anyhow::bail!("Missing owner address in topic1 when parsing burn event"),
316 };
317
318 let tick_lower = match log.topics.get(2).and_then(|t| t.as_ref()) {
320 Some(topic) => {
321 let tick_lower_bytes: [u8; 32] = topic.as_ref().try_into()?;
322 i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?)
323 }
324 None => anyhow::bail!("Missing tickLower in topic2 when parsing burn event"),
325 };
326
327 let tick_upper = match log.topics.get(3).and_then(|t| t.as_ref()) {
329 Some(topic) => {
330 let tick_upper_bytes: [u8; 32] = topic.as_ref().try_into()?;
331 i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?)
332 }
333 None => anyhow::bail!("Missing tickUpper in topic3 when parsing burn event"),
334 };
335
336 if let Some(data) = &log.data {
337 let data_bytes = data.as_ref();
338
339 if data_bytes.len() < 3 * 32 {
341 anyhow::bail!("Burn event data is too short");
342 }
343
344 let decoded = match <BurnEventData as SolType>::abi_decode(data_bytes) {
346 Ok(decoded) => decoded,
347 Err(e) => anyhow::bail!("Failed to decode burn event data: {e}"),
348 };
349
350 Ok(BurnEvent::new(
351 extract_block_number(&log)?,
352 extract_transaction_hash(&log)?,
353 extract_transaction_index(&log)?,
354 extract_log_index(&log)?,
355 owner,
356 tick_lower,
357 tick_upper,
358 decoded.amount,
359 decoded.amount0,
360 decoded.amount1,
361 ))
362 } else {
363 Err(anyhow::anyhow!("Missing data in burn event log"))
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use rstest::*;
370
371 use super::*;
372
373 #[fixture]
374 fn mint_event_log() -> Log {
375 serde_json::from_str(r#"{
376 "removed": null,
377 "log_index": "0xa",
378 "transaction_index": "0x5",
379 "transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
380 "block_hash": null,
381 "block_number": "0x1581756",
382 "address": null,
383 "data": "0x000000000000000000000000f5a96d43e4b9a2c47f302b54d006d7e20f038658000000000000000000000000000000000000000000000028c8b4995ae1ad0e9e000000000000000000000000000000000000000000000000000009423c32486c0000000000000000000000000000000000000000000000bb5bc19aa32e5d05b4",
384 "topics": [
385 "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde",
386 "0x000000000000000000000000a69babef1ca67a37ffaf7a485dfff3382056e78c",
387 "0x00000000000000000000000000000000000000000000000000000000000304e4",
388 "0x00000000000000000000000000000000000000000000000000000000000304ee"
389 ]
390 }"#).unwrap()
391 }
392
393 #[rstest]
394 fn test_parse_mint_event(mint_event_log: Log) {
395 let result = parse_mint_event(mint_event_log);
396 assert!(result.is_ok());
397 let mint_event = result.unwrap();
398
399 assert_eq!(mint_event.block_number, 0x1581756);
400 assert_eq!(
401 mint_event.owner.to_string().to_lowercase(),
402 "0xa69babef1ca67a37ffaf7a485dfff3382056e78c"
403 );
404 assert_eq!(mint_event.tick_lower, 197860); assert_eq!(mint_event.tick_upper, 197870); assert_eq!(
407 mint_event.sender.to_string().to_lowercase(),
408 "0xf5a96d43e4b9a2c47f302b54d006d7e20f038658"
409 );
410 assert_eq!(mint_event.amount, 0x28c8b4995ae1ad0e9e);
411 assert_eq!(mint_event.amount0.to_string(), "10180082419820");
412 assert_eq!(mint_event.amount1.to_string(), "3456152877537290945972");
413 }
414
415 #[rstest]
416 fn test_parse_mint_event_missing_data() {
417 let mut log = mint_event_log();
418 log.data = None;
419
420 let result = parse_mint_event(log);
421 assert!(result.is_err());
422 assert!(result.unwrap_err().to_string().contains("Missing data"));
423 }
424
425 #[rstest]
426 fn test_parse_mint_event_missing_topics() {
427 let mut log = mint_event_log();
428
429 log.topics.truncate(1);
431 let result = parse_mint_event(log.clone());
432 assert!(result.is_err());
433 assert!(result.unwrap_err().to_string().contains("Missing owner"));
434
435 log = mint_event_log();
437 log.topics.truncate(2);
438 let result = parse_mint_event(log.clone());
439 assert!(result.is_err());
440 assert!(
441 result
442 .unwrap_err()
443 .to_string()
444 .contains("Missing tickLower")
445 );
446
447 log = mint_event_log();
449 log.topics.truncate(3);
450 let result = parse_mint_event(log);
451 assert!(result.is_err());
452 assert!(
453 result
454 .unwrap_err()
455 .to_string()
456 .contains("Missing tickUpper")
457 );
458 }
459}