nautilus_blockchain/exchanges/parsing/pancakeswap_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 =
34 "19b47279256b2a23a1665c810c8d55a1758940ee09377d4f8d26497a3577dc83";
35
36sol! {
38 struct SwapEventData {
39 int256 amount0;
40 int256 amount1;
41 uint160 sqrt_price_x96;
42 uint128 liquidity;
43 int24 tick;
44 uint128 protocol_fees_token0;
45 uint128 protocol_fees_token1;
46 }
47}
48
49pub fn parse_swap_event_hypersync(dex: SharedDex, log: &HypersyncLog) -> anyhow::Result<SwapEvent> {
62 validate_event_signature_hash("SwapEvent", SWAP_EVENT_SIGNATURE_HASH, log)?;
63
64 let sender = extract_address_from_topic(log, 1, "sender")?;
65 let recipient = extract_address_from_topic(log, 2, "recipient")?;
66
67 if let Some(data) = &log.data {
68 let data_bytes = data.as_ref();
69
70 if data_bytes.len() < 7 * 32 {
71 anyhow::bail!("Swap event data is too short");
72 }
73
74 let decoded = match <SwapEventData as SolType>::abi_decode(data_bytes) {
75 Ok(decoded) => decoded,
76 Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
77 };
78 let pool_address = Address::from_slice(
79 log.address
80 .clone()
81 .expect("Contract address should be set in logs")
82 .as_ref(),
83 );
84 let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
85 Ok(SwapEvent::new(
86 dex,
87 pool_identifier,
88 extract_block_number(log)?,
89 extract_transaction_hash(log)?,
90 extract_transaction_index(log)?,
91 extract_log_index(log)?,
92 sender,
93 recipient,
94 decoded.amount0,
95 decoded.amount1,
96 decoded.sqrt_price_x96,
97 decoded.liquidity,
98 decoded.tick.as_i32(),
99 ))
100 } else {
101 Err(anyhow::anyhow!("Missing data in swap event log"))
102 }
103}
104
105pub fn parse_swap_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<SwapEvent> {
111 rpc_log::validate_event_signature(log, SWAP_EVENT_SIGNATURE_HASH, "Swap")?;
112
113 let sender = rpc_log::extract_address_from_topic(log, 1, "sender")?;
114 let recipient = rpc_log::extract_address_from_topic(log, 2, "recipient")?;
115
116 let data_bytes = rpc_log::extract_data_bytes(log)?;
117
118 if data_bytes.len() < 7 * 32 {
119 anyhow::bail!("Swap event data is too short");
120 }
121
122 let decoded = match <SwapEventData as SolType>::abi_decode(&data_bytes) {
123 Ok(decoded) => decoded,
124 Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
125 };
126
127 let pool_address = rpc_log::extract_address(log)?;
128 let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
129 Ok(SwapEvent::new(
130 dex,
131 pool_identifier,
132 rpc_log::extract_block_number(log)?,
133 rpc_log::extract_transaction_hash(log)?,
134 rpc_log::extract_transaction_index(log)?,
135 rpc_log::extract_log_index(log)?,
136 sender,
137 recipient,
138 decoded.amount0,
139 decoded.amount1,
140 decoded.sqrt_price_x96,
141 decoded.liquidity,
142 decoded.tick.as_i32(),
143 ))
144}
145
146#[cfg(test)]
147mod tests {
148 use std::sync::Arc;
149
150 use alloy::primitives::{I256, U160, U256};
151 use rstest::*;
152
153 use super::*;
154 use crate::exchanges::bsc;
155
156 const HYPERSYNC_SWAP_LOG: &str =
159 include_str!("../../../../test_data/pancakeswap_v3_swap_hypersync.json");
160 const RPC_SWAP_LOG: &str = include_str!("../../../../test_data/pancakeswap_v3_swap_rpc.json");
161
162 #[fixture]
163 fn hypersync_log() -> HypersyncLog {
164 serde_json::from_str(HYPERSYNC_SWAP_LOG).expect("Failed to deserialize HyperSync log")
165 }
166
167 #[fixture]
168 fn rpc_log() -> RpcLog {
169 serde_json::from_str(RPC_SWAP_LOG).expect("Failed to deserialize RPC log")
170 }
171
172 #[rstest]
173 fn test_parse_swap_event_hypersync(hypersync_log: HypersyncLog) {
174 let dex = Arc::clone(&bsc::PANCAKESWAP_V3.dex);
175 let event = parse_swap_event_hypersync(dex, &hypersync_log).unwrap();
176
177 assert_eq!(
178 event.pool_identifier.to_string(),
179 "0x172fcD41E0913e95784454622d1c3724f546f849"
180 );
181 assert_eq!(
182 event.sender.to_string().to_lowercase(),
183 "0x7eded5ce04fd9bb6d125a0a470cc3ffcd972e182"
184 );
185 assert_eq!(
186 event.receiver.to_string().to_lowercase(),
187 "0x7eded5ce04fd9bb6d125a0a470cc3ffcd972e182"
188 );
189 assert_eq!(
190 event.amount0,
191 I256::try_from(2291588381489685660_i128).unwrap()
192 );
193 let expected_amount1 = I256::from_raw(
194 U256::from_str_radix(
195 "fffffffffffffffffffffffffffffffffffffffffffffffffff22743d8dee163",
196 16,
197 )
198 .unwrap(),
199 );
200 assert_eq!(event.amount1, expected_amount1);
201 let expected_sqrt_price = U160::from_str_radix("a8edeae49c411da42257f71", 16).unwrap();
202 assert_eq!(event.sqrt_price_x96, expected_sqrt_price);
203 let expected_liquidity = u128::from_str_radix("310fdcabce7b0096dfc84", 16).unwrap();
204 assert_eq!(event.liquidity, expected_liquidity);
205 assert_eq!(event.tick, -63769);
206 assert_eq!(event.block_number, 105495649);
207 }
208
209 #[rstest]
210 fn test_parse_swap_event_rpc(rpc_log: RpcLog) {
211 let dex = Arc::clone(&bsc::PANCAKESWAP_V3.dex);
212 let event = parse_swap_event_rpc(dex, &rpc_log).unwrap();
213
214 assert_eq!(
215 event.pool_identifier.to_string(),
216 "0x172fcD41E0913e95784454622d1c3724f546f849"
217 );
218 assert_eq!(
219 event.amount0,
220 I256::try_from(2291588381489685660_i128).unwrap()
221 );
222 let expected_amount1 = I256::from_raw(
223 U256::from_str_radix(
224 "fffffffffffffffffffffffffffffffffffffffffffffffffff22743d8dee163",
225 16,
226 )
227 .unwrap(),
228 );
229 assert_eq!(event.amount1, expected_amount1);
230 assert_eq!(event.tick, -63769);
231 assert_eq!(event.block_number, 105495649);
232 }
233
234 #[rstest]
235 fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
236 let dex = Arc::clone(&bsc::PANCAKESWAP_V3.dex);
237 let event_hypersync = parse_swap_event_hypersync(Arc::clone(&dex), &hypersync_log).unwrap();
238 let event_rpc = parse_swap_event_rpc(dex, &rpc_log).unwrap();
239
240 assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
241 assert_eq!(event_hypersync.sender, event_rpc.sender);
242 assert_eq!(event_hypersync.receiver, event_rpc.receiver);
243 assert_eq!(event_hypersync.amount0, event_rpc.amount0);
244 assert_eq!(event_hypersync.amount1, event_rpc.amount1);
245 assert_eq!(event_hypersync.sqrt_price_x96, event_rpc.sqrt_price_x96);
246 assert_eq!(event_hypersync.liquidity, event_rpc.liquidity);
247 assert_eq!(event_hypersync.tick, event_rpc.tick);
248 assert_eq!(event_hypersync.block_number, event_rpc.block_number);
249 assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
250 }
251
252 #[rstest]
253 fn test_rejects_uniswap_v3_length_data() {
254 let mut value: serde_json::Value = serde_json::from_str(HYPERSYNC_SWAP_LOG).unwrap();
257 let data = value["data"].as_str().unwrap();
258 let truncated = data[..2 + 5 * 64].to_string();
260 value["data"] = serde_json::Value::String(truncated);
261 let log: HypersyncLog = serde_json::from_value(value).unwrap();
262
263 let dex = Arc::clone(&bsc::PANCAKESWAP_V3.dex);
264 let err = parse_swap_event_hypersync(dex, &log).unwrap_err();
265 assert!(err.to_string().contains("too short"));
266 }
267
268 #[rstest]
269 fn test_rejects_uniswap_v3_swap_topic() {
270 let uniswap_v3_topic = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
273 let log_json = HYPERSYNC_SWAP_LOG.replace(SWAP_EVENT_SIGNATURE_HASH, uniswap_v3_topic);
274 let log: HypersyncLog = serde_json::from_str(&log_json).unwrap();
275
276 let dex = Arc::clone(&bsc::PANCAKESWAP_V3.dex);
277 let result = parse_swap_event_hypersync(dex, &log);
278 assert!(result.is_err());
279 }
280}