nautilus_blockchain/exchanges/parsing/uniswap_v3/
swap.rs1use alloy::{dyn_abi::SolType, primitives::Address, sol};
17use nautilus_model::defi::{PoolIdentifier, SharedDex, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21 events::swap::SwapEvent,
22 hypersync::{
23 HypersyncLog,
24 log::{
25 extract_address_from_topic, extract_block_number, extract_log_index,
26 extract_transaction_hash, extract_transaction_index, validate_event_signature_hash,
27 },
28 },
29 rpc::log as rpc_log,
30};
31
32const SWAP_EVENT_SIGNATURE_HASH: &str =
33 "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
34
35sol! {
39 struct SwapEventData {
40 int256 amount0;
41 int256 amount1;
42 uint160 sqrt_price_x96;
43 uint128 liquidity;
44 int24 tick;
45 }
46}
47
48pub fn parse_swap_event_hypersync(dex: SharedDex, log: &HypersyncLog) -> anyhow::Result<SwapEvent> {
58 validate_event_signature_hash("SwapEvent", SWAP_EVENT_SIGNATURE_HASH, log)?;
59
60 let sender = extract_address_from_topic(log, 1, "sender")?;
61 let recipient = extract_address_from_topic(log, 2, "recipient")?;
62
63 if let Some(data) = &log.data {
64 let data_bytes = data.as_ref();
65
66 if data_bytes.len() < 5 * 32 {
68 anyhow::bail!("Swap event data is too short");
69 }
70
71 let decoded = match <SwapEventData as SolType>::abi_decode(data_bytes) {
73 Ok(decoded) => decoded,
74 Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
75 };
76 let _ = decoded.amount0;
77 let pool_address = Address::from_slice(
78 log.address
79 .clone()
80 .expect("Contract address should be set in logs")
81 .as_ref(),
82 );
83 let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
84 Ok(SwapEvent::new(
85 dex,
86 pool_identifier,
87 extract_block_number(log)?,
88 extract_transaction_hash(log)?,
89 extract_transaction_index(log)?,
90 extract_log_index(log)?,
91 sender,
92 recipient,
93 decoded.amount0,
94 decoded.amount1,
95 decoded.sqrt_price_x96,
96 decoded.liquidity,
97 decoded.tick.as_i32(),
98 ))
99 } else {
100 Err(anyhow::anyhow!("Missing data in swap event log"))
101 }
102}
103
104pub fn parse_swap_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<SwapEvent> {
110 rpc_log::validate_event_signature(log, SWAP_EVENT_SIGNATURE_HASH, "Swap")?;
111
112 let sender = rpc_log::extract_address_from_topic(log, 1, "sender")?;
113 let recipient = rpc_log::extract_address_from_topic(log, 2, "recipient")?;
114
115 let data_bytes = rpc_log::extract_data_bytes(log)?;
116
117 if data_bytes.len() < 5 * 32 {
119 anyhow::bail!("Swap event data is too short");
120 }
121
122 let decoded = match <SwapEventData as SolType>::abi_decode(&data_bytes) {
124 Ok(decoded) => decoded,
125 Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
126 };
127
128 let pool_address = rpc_log::extract_address(log)?;
129 let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
130 Ok(SwapEvent::new(
131 dex,
132 pool_identifier,
133 rpc_log::extract_block_number(log)?,
134 rpc_log::extract_transaction_hash(log)?,
135 rpc_log::extract_transaction_index(log)?,
136 rpc_log::extract_log_index(log)?,
137 sender,
138 recipient,
139 decoded.amount0,
140 decoded.amount1,
141 decoded.sqrt_price_x96,
142 decoded.liquidity,
143 decoded.tick.as_i32(),
144 ))
145}
146
147#[cfg(test)]
148mod tests {
149 use std::sync::Arc;
150
151 use alloy::primitives::{I256, U160, U256};
152 use rstest::*;
153 use serde_json::json;
154
155 use super::*;
156 use crate::exchanges::arbitrum;
157
158 #[fixture]
166 fn hypersync_log() -> HypersyncLog {
167 let log_json = json!({
168 "removed": null,
169 "log_index": "0x6",
170 "transaction_index": "0x3",
171 "transaction_hash": "0x381ae1c1b65bba31abdfc68ef6b3e3e49913161a15398ccff3b242b05473e720",
172 "block_hash": null,
173 "block_number": "0x17513444",
174 "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
175 "data": "0xffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe000000000000000000000000000000000000000000000000000e2a274937d6380000000000000000000000000000000000000000003d5fe159ea44896552c1cd000000000000000000000000000000000000000000000074009aac72ba0a9b1cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffddf2d",
176 "topics": [
177 "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
178 "0x0000000000000000000000009da4a7d3cf502337797ea37724f7afc426377119",
179 "0x000000000000000000000000d491076c7316bc28fd4d35e3da9ab5286d079250"
180 ]
181 });
182 serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
183 }
184
185 #[fixture]
187 fn rpc_log() -> RpcLog {
188 let log_json = json!({
189 "removed": false,
190 "logIndex": "0x6",
191 "transactionIndex": "0x3",
192 "transactionHash": "0x381ae1c1b65bba31abdfc68ef6b3e3e49913161a15398ccff3b242b05473e720",
193 "blockHash": "0x43082eabb648a3b87bd22abf7ec645a97e6e7f099dcc18894830c70d85675fae",
194 "blockNumber": "0x17513444",
195 "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
196 "data": "0xffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe000000000000000000000000000000000000000000000000000e2a274937d6380000000000000000000000000000000000000000003d5fe159ea44896552c1cd000000000000000000000000000000000000000000000074009aac72ba0a9b1cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffddf2d",
197 "topics": [
198 "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
199 "0x0000000000000000000000009da4a7d3cf502337797ea37724f7afc426377119",
200 "0x000000000000000000000000d491076c7316bc28fd4d35e3da9ab5286d079250"
201 ]
202 });
203 serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
204 }
205
206 #[rstest]
207 fn test_parse_swap_event_hypersync(hypersync_log: HypersyncLog) {
208 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
209 let event = parse_swap_event_hypersync(dex, &hypersync_log).unwrap();
210
211 assert_eq!(
212 event.pool_identifier.to_string(),
213 "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
214 );
215 assert_eq!(
216 event.sender.to_string().to_lowercase(),
217 "0x9da4a7d3cf502337797ea37724f7afc426377119"
218 );
219 assert_eq!(
220 event.receiver.to_string().to_lowercase(),
221 "0xd491076c7316bc28fd4d35e3da9ab5286d079250"
222 );
223 let expected_amount0 = I256::from_raw(
224 U256::from_str_radix(
225 "ffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe",
226 16,
227 )
228 .unwrap(),
229 );
230 assert_eq!(event.amount0, expected_amount0);
231 let expected_amount1 = I256::from_raw(U256::from_str_radix("0e2a274937d638", 16).unwrap());
232 assert_eq!(event.amount1, expected_amount1);
233 let expected_sqrt_price = U160::from_str_radix("3d5fe159ea44896552c1cd", 16).unwrap();
234 assert_eq!(event.sqrt_price_x96, expected_sqrt_price);
235 let expected_liquidity = u128::from_str_radix("74009aac72ba0a9b1c", 16).unwrap();
236 assert_eq!(event.liquidity, expected_liquidity);
237 assert_eq!(event.tick, -139475);
238 assert_eq!(event.block_number, 391197764);
239 }
240
241 #[rstest]
242 fn test_parse_swap_event_rpc(rpc_log: RpcLog) {
243 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
244 let event = parse_swap_event_rpc(dex, &rpc_log).unwrap();
245
246 assert_eq!(
247 event.pool_identifier.to_string(),
248 "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
249 );
250 assert_eq!(
251 event.sender.to_string().to_lowercase(),
252 "0x9da4a7d3cf502337797ea37724f7afc426377119"
253 );
254 assert_eq!(
255 event.receiver.to_string().to_lowercase(),
256 "0xd491076c7316bc28fd4d35e3da9ab5286d079250"
257 );
258 let expected_amount0 = I256::from_raw(
259 U256::from_str_radix(
260 "ffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe",
261 16,
262 )
263 .unwrap(),
264 );
265 assert_eq!(event.amount0, expected_amount0);
266 let expected_amount1 = I256::from_raw(U256::from_str_radix("0e2a274937d638", 16).unwrap());
267 assert_eq!(event.amount1, expected_amount1);
268 let expected_sqrt_price = U160::from_str_radix("3d5fe159ea44896552c1cd", 16).unwrap();
269 assert_eq!(event.sqrt_price_x96, expected_sqrt_price);
270 let expected_liquidity = u128::from_str_radix("74009aac72ba0a9b1c", 16).unwrap();
271 assert_eq!(event.liquidity, expected_liquidity);
272 assert_eq!(event.tick, -139475);
273 assert_eq!(event.block_number, 391197764);
274 }
275
276 #[rstest]
277 fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
278 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
279 let event_hypersync = parse_swap_event_hypersync(Arc::clone(&dex), &hypersync_log).unwrap();
280 let event_rpc = parse_swap_event_rpc(dex, &rpc_log).unwrap();
281
282 assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
283 assert_eq!(event_hypersync.sender, event_rpc.sender);
284 assert_eq!(event_hypersync.receiver, event_rpc.receiver);
285 assert_eq!(event_hypersync.amount0, event_rpc.amount0);
286 assert_eq!(event_hypersync.amount1, event_rpc.amount1);
287 assert_eq!(event_hypersync.sqrt_price_x96, event_rpc.sqrt_price_x96);
288 assert_eq!(event_hypersync.liquidity, event_rpc.liquidity);
289 assert_eq!(event_hypersync.tick, event_rpc.tick);
290 assert_eq!(event_hypersync.block_number, event_rpc.block_number);
291 assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
292 }
293}