nautilus_blockchain/exchanges/parsing/uniswap_v3/
burn.rs1use alloy::{dyn_abi::SolType, primitives::Address, sol};
17use nautilus_model::defi::{PoolIdentifier, SharedDex, rpc::RpcLog};
18use ustr::Ustr;
19
20use crate::{
21 events::burn::BurnEvent,
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 BURN_EVENT_SIGNATURE_HASH: &str =
33 "0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c";
34
35sol! {
39 struct BurnEventData {
40 uint128 amount;
41 uint256 amount0;
42 uint256 amount1;
43 }
44}
45
46pub fn parse_burn_event_hypersync(dex: SharedDex, log: &HypersyncLog) -> anyhow::Result<BurnEvent> {
56 validate_event_signature_hash("Burn", BURN_EVENT_SIGNATURE_HASH, log)?;
57
58 let owner = extract_address_from_topic(log, 1, "owner")?;
59
60 let tick_lower = match log.topics.get(2).and_then(|t| t.as_ref()) {
62 Some(topic) => {
63 let tick_lower_bytes: [u8; 32] = topic.as_ref().try_into()?;
64 i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?)
65 }
66 None => anyhow::bail!("Missing tickLower in topic2 when parsing burn event"),
67 };
68
69 let tick_upper = match log.topics.get(3).and_then(|t| t.as_ref()) {
71 Some(topic) => {
72 let tick_upper_bytes: [u8; 32] = topic.as_ref().try_into()?;
73 i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?)
74 }
75 None => anyhow::bail!("Missing tickUpper in topic3 when parsing burn event"),
76 };
77
78 if let Some(data) = &log.data {
79 let data_bytes = data.as_ref();
80
81 if data_bytes.len() < 3 * 32 {
83 anyhow::bail!("Burn event data is too short");
84 }
85
86 let decoded = match <BurnEventData as SolType>::abi_decode(data_bytes) {
88 Ok(decoded) => decoded,
89 Err(e) => anyhow::bail!("Failed to decode burn event data: {e}"),
90 };
91
92 let pool_address = Address::from_slice(
93 log.address
94 .clone()
95 .expect("Contract address should be set in logs")
96 .as_ref(),
97 );
98 let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
99 Ok(BurnEvent::new(
100 dex,
101 pool_identifier,
102 extract_block_number(log)?,
103 extract_transaction_hash(log)?,
104 extract_transaction_index(log)?,
105 extract_log_index(log)?,
106 owner,
107 tick_lower,
108 tick_upper,
109 decoded.amount,
110 decoded.amount0,
111 decoded.amount1,
112 ))
113 } else {
114 Err(anyhow::anyhow!("Missing data in burn event log"))
115 }
116}
117
118pub fn parse_burn_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<BurnEvent> {
124 rpc_log::validate_event_signature(log, BURN_EVENT_SIGNATURE_HASH, "Burn")?;
125
126 let owner = rpc_log::extract_address_from_topic(log, 1, "owner")?;
127
128 let tick_lower_bytes = rpc_log::extract_topic_bytes(log, 2)?;
130 let tick_lower = i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?);
131
132 let tick_upper_bytes = rpc_log::extract_topic_bytes(log, 3)?;
134 let tick_upper = i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?);
135
136 let data_bytes = rpc_log::extract_data_bytes(log)?;
137
138 if data_bytes.len() < 3 * 32 {
140 anyhow::bail!("Burn event data is too short");
141 }
142
143 let decoded = match <BurnEventData as SolType>::abi_decode(&data_bytes) {
145 Ok(decoded) => decoded,
146 Err(e) => anyhow::bail!("Failed to decode burn event data: {e}"),
147 };
148
149 let pool_address = rpc_log::extract_address(log)?;
150 let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
151 Ok(BurnEvent::new(
152 dex,
153 pool_identifier,
154 rpc_log::extract_block_number(log)?,
155 rpc_log::extract_transaction_hash(log)?,
156 rpc_log::extract_transaction_index(log)?,
157 rpc_log::extract_log_index(log)?,
158 owner,
159 tick_lower,
160 tick_upper,
161 decoded.amount,
162 decoded.amount0,
163 decoded.amount1,
164 ))
165}
166
167#[cfg(test)]
168mod tests {
169 use std::sync::Arc;
170
171 use alloy::primitives::U256;
172 use rstest::*;
173 use serde_json::json;
174
175 use super::*;
176 use crate::exchanges::arbitrum;
177
178 #[fixture]
184 fn hypersync_log() -> HypersyncLog {
185 let log_json = json!({
186 "removed": null,
187 "log_index": "0xd",
188 "transaction_index": "0x5",
189 "transaction_hash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
190 "block_hash": null,
191 "block_number": "0x175a6484",
192 "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
193 "data": "0x00000000000000000000000000000000000000000002bc997c8ea58d3a8521ec0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065b33ff853688eb",
194 "topics": [
195 "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c",
196 "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
197 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
198 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
199 ]
200 });
201 serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
202 }
203
204 #[fixture]
206 fn rpc_log() -> RpcLog {
207 let log_json = json!({
208 "removed": false,
209 "logIndex": "0xd",
210 "transactionIndex": "0x5",
211 "transactionHash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
212 "blockHash": "0xe925eaa1f5178ceedfa24043a974edb928ddab7195600b6b99ff5403fbf13c8b",
213 "blockNumber": "0x175a6484",
214 "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
215 "data": "0x00000000000000000000000000000000000000000002bc997c8ea58d3a8521ec0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065b33ff853688eb",
216 "topics": [
217 "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c",
218 "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
219 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
220 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
221 ]
222 });
223 serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
224 }
225
226 #[rstest]
227 fn test_parse_burn_event_hypersync(hypersync_log: HypersyncLog) {
228 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
229 let event = parse_burn_event_hypersync(dex, &hypersync_log).unwrap();
230
231 assert_eq!(
232 event.pool_identifier.to_string(),
233 "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
234 );
235 assert_eq!(
236 event.owner.to_string().to_lowercase(),
237 "0xc36442b4a4522e871399cd717abdd847ab11fe88"
238 );
239 assert_eq!(event.tick_lower, -139767);
240 assert_eq!(event.tick_upper, -139764);
241 let expected_amount = u128::from_str_radix("2bc997c8ea58d3a8521ec", 16).unwrap();
242 assert_eq!(event.amount, expected_amount);
243 assert_eq!(event.amount0, U256::ZERO);
244 let expected_amount1 = U256::from_str_radix("65b33ff853688eb", 16).unwrap();
245 assert_eq!(event.amount1, expected_amount1);
246 assert_eq!(event.block_number, 391799940);
247 }
248
249 #[rstest]
250 fn test_parse_burn_event_rpc(rpc_log: RpcLog) {
251 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
252 let event = parse_burn_event_rpc(dex, &rpc_log).unwrap();
253
254 assert_eq!(
255 event.pool_identifier.to_string(),
256 "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
257 );
258 assert_eq!(
259 event.owner.to_string().to_lowercase(),
260 "0xc36442b4a4522e871399cd717abdd847ab11fe88"
261 );
262 assert_eq!(event.tick_lower, -139767);
263 assert_eq!(event.tick_upper, -139764);
264 let expected_amount = u128::from_str_radix("2bc997c8ea58d3a8521ec", 16).unwrap();
265 assert_eq!(event.amount, expected_amount);
266 assert_eq!(event.amount0, U256::ZERO);
267 let expected_amount1 = U256::from_str_radix("65b33ff853688eb", 16).unwrap();
268 assert_eq!(event.amount1, expected_amount1);
269 assert_eq!(event.block_number, 391799940);
270 }
271
272 #[rstest]
273 fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
274 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
275 let event_hypersync = parse_burn_event_hypersync(Arc::clone(&dex), &hypersync_log).unwrap();
276 let event_rpc = parse_burn_event_rpc(dex, &rpc_log).unwrap();
277
278 assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
279 assert_eq!(event_hypersync.owner, event_rpc.owner);
280 assert_eq!(event_hypersync.tick_lower, event_rpc.tick_lower);
281 assert_eq!(event_hypersync.tick_upper, event_rpc.tick_upper);
282 assert_eq!(event_hypersync.amount, event_rpc.amount);
283 assert_eq!(event_hypersync.amount0, event_rpc.amount0);
284 assert_eq!(event_hypersync.amount1, event_rpc.amount1);
285 assert_eq!(event_hypersync.block_number, event_rpc.block_number);
286 assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
287 }
288}