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 helpers::{
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::helpers as rpc_helpers,
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_helpers::validate_event_signature(log, SWAP_EVENT_SIGNATURE_HASH, "Swap")?;
112
113 let sender = rpc_helpers::extract_address_from_topic(log, 1, "sender")?;
114 let recipient = rpc_helpers::extract_address_from_topic(log, 2, "recipient")?;
115
116 let data_bytes = rpc_helpers::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_helpers::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_helpers::extract_block_number(log)?,
133 rpc_helpers::extract_transaction_hash(log)?,
134 rpc_helpers::extract_transaction_index(log)?,
135 rpc_helpers::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 alloy::primitives::{I256, U160, U256};
149 use rstest::*;
150
151 use super::*;
152 use crate::exchanges::bsc;
153
154 const HYPERSYNC_SWAP_LOG: &str =
157 include_str!("../../../../test_data/pancakeswap_v3_swap_hypersync.json");
158 const RPC_SWAP_LOG: &str = include_str!("../../../../test_data/pancakeswap_v3_swap_rpc.json");
159
160 #[fixture]
161 fn hypersync_log() -> HypersyncLog {
162 serde_json::from_str(HYPERSYNC_SWAP_LOG).expect("Failed to deserialize HyperSync log")
163 }
164
165 #[fixture]
166 fn rpc_log() -> RpcLog {
167 serde_json::from_str(RPC_SWAP_LOG).expect("Failed to deserialize RPC log")
168 }
169
170 #[rstest]
171 fn test_parse_swap_event_hypersync(hypersync_log: HypersyncLog) {
172 let dex = bsc::PANCAKESWAP_V3.dex.clone();
173 let event = parse_swap_event_hypersync(dex, &hypersync_log).unwrap();
174
175 assert_eq!(
176 event.pool_identifier.to_string(),
177 "0x172fcD41E0913e95784454622d1c3724f546f849"
178 );
179 assert_eq!(
180 event.sender.to_string().to_lowercase(),
181 "0x7eded5ce04fd9bb6d125a0a470cc3ffcd972e182"
182 );
183 assert_eq!(
184 event.receiver.to_string().to_lowercase(),
185 "0x7eded5ce04fd9bb6d125a0a470cc3ffcd972e182"
186 );
187 assert_eq!(
188 event.amount0,
189 I256::try_from(2291588381489685660_i128).unwrap()
190 );
191 let expected_amount1 = I256::from_raw(
192 U256::from_str_radix(
193 "fffffffffffffffffffffffffffffffffffffffffffffffffff22743d8dee163",
194 16,
195 )
196 .unwrap(),
197 );
198 assert_eq!(event.amount1, expected_amount1);
199 let expected_sqrt_price = U160::from_str_radix("a8edeae49c411da42257f71", 16).unwrap();
200 assert_eq!(event.sqrt_price_x96, expected_sqrt_price);
201 let expected_liquidity = u128::from_str_radix("310fdcabce7b0096dfc84", 16).unwrap();
202 assert_eq!(event.liquidity, expected_liquidity);
203 assert_eq!(event.tick, -63769);
204 assert_eq!(event.block_number, 105495649);
205 }
206
207 #[rstest]
208 fn test_parse_swap_event_rpc(rpc_log: RpcLog) {
209 let dex = bsc::PANCAKESWAP_V3.dex.clone();
210 let event = parse_swap_event_rpc(dex, &rpc_log).unwrap();
211
212 assert_eq!(
213 event.pool_identifier.to_string(),
214 "0x172fcD41E0913e95784454622d1c3724f546f849"
215 );
216 assert_eq!(
217 event.amount0,
218 I256::try_from(2291588381489685660_i128).unwrap()
219 );
220 let expected_amount1 = I256::from_raw(
221 U256::from_str_radix(
222 "fffffffffffffffffffffffffffffffffffffffffffffffffff22743d8dee163",
223 16,
224 )
225 .unwrap(),
226 );
227 assert_eq!(event.amount1, expected_amount1);
228 assert_eq!(event.tick, -63769);
229 assert_eq!(event.block_number, 105495649);
230 }
231
232 #[rstest]
233 fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
234 let dex = bsc::PANCAKESWAP_V3.dex.clone();
235 let event_hypersync = parse_swap_event_hypersync(dex.clone(), &hypersync_log).unwrap();
236 let event_rpc = parse_swap_event_rpc(dex, &rpc_log).unwrap();
237
238 assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
239 assert_eq!(event_hypersync.sender, event_rpc.sender);
240 assert_eq!(event_hypersync.receiver, event_rpc.receiver);
241 assert_eq!(event_hypersync.amount0, event_rpc.amount0);
242 assert_eq!(event_hypersync.amount1, event_rpc.amount1);
243 assert_eq!(event_hypersync.sqrt_price_x96, event_rpc.sqrt_price_x96);
244 assert_eq!(event_hypersync.liquidity, event_rpc.liquidity);
245 assert_eq!(event_hypersync.tick, event_rpc.tick);
246 assert_eq!(event_hypersync.block_number, event_rpc.block_number);
247 assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
248 }
249
250 #[rstest]
251 fn test_rejects_uniswap_v3_length_data() {
252 let mut value: serde_json::Value = serde_json::from_str(HYPERSYNC_SWAP_LOG).unwrap();
255 let data = value["data"].as_str().unwrap();
256 let truncated = data[..2 + 5 * 64].to_string();
258 value["data"] = serde_json::Value::String(truncated);
259 let log: HypersyncLog = serde_json::from_value(value).unwrap();
260
261 let dex = bsc::PANCAKESWAP_V3.dex.clone();
262 let err = parse_swap_event_hypersync(dex, &log).unwrap_err();
263 assert!(err.to_string().contains("too short"));
264 }
265
266 #[rstest]
267 fn test_rejects_uniswap_v3_swap_topic() {
268 let uniswap_v3_topic = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
271 let log_json = HYPERSYNC_SWAP_LOG.replace(SWAP_EVENT_SIGNATURE_HASH, uniswap_v3_topic);
272 let log: HypersyncLog = serde_json::from_str(&log_json).unwrap();
273
274 let dex = bsc::PANCAKESWAP_V3.dex.clone();
275 let result = parse_swap_event_hypersync(dex, &log);
276 assert!(result.is_err());
277 }
278}