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 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 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_helpers::validate_event_signature(log, BURN_EVENT_SIGNATURE_HASH, "Burn")?;
125
126 let owner = rpc_helpers::extract_address_from_topic(log, 1, "owner")?;
127
128 let tick_lower_bytes = rpc_helpers::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_helpers::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_helpers::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_helpers::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_helpers::extract_block_number(log)?,
155 rpc_helpers::extract_transaction_hash(log)?,
156 rpc_helpers::extract_transaction_index(log)?,
157 rpc_helpers::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 alloy::primitives::U256;
170 use rstest::*;
171 use serde_json::json;
172
173 use super::*;
174 use crate::exchanges::arbitrum;
175
176 #[fixture]
182 fn hypersync_log() -> HypersyncLog {
183 let log_json = json!({
184 "removed": null,
185 "log_index": "0xd",
186 "transaction_index": "0x5",
187 "transaction_hash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
188 "block_hash": null,
189 "block_number": "0x175a6484",
190 "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
191 "data": "0x00000000000000000000000000000000000000000002bc997c8ea58d3a8521ec0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065b33ff853688eb",
192 "topics": [
193 "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c",
194 "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
195 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
196 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
197 ]
198 });
199 serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
200 }
201
202 #[fixture]
204 fn rpc_log() -> RpcLog {
205 let log_json = json!({
206 "removed": false,
207 "logIndex": "0xd",
208 "transactionIndex": "0x5",
209 "transactionHash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
210 "blockHash": "0xe925eaa1f5178ceedfa24043a974edb928ddab7195600b6b99ff5403fbf13c8b",
211 "blockNumber": "0x175a6484",
212 "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
213 "data": "0x00000000000000000000000000000000000000000002bc997c8ea58d3a8521ec0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065b33ff853688eb",
214 "topics": [
215 "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c",
216 "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
217 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
218 "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
219 ]
220 });
221 serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
222 }
223
224 #[rstest]
225 fn test_parse_burn_event_hypersync(hypersync_log: HypersyncLog) {
226 let dex = arbitrum::UNISWAP_V3.dex.clone();
227 let event = parse_burn_event_hypersync(dex, hypersync_log).unwrap();
228
229 assert_eq!(
230 event.pool_identifier.to_string(),
231 "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
232 );
233 assert_eq!(
234 event.owner.to_string().to_lowercase(),
235 "0xc36442b4a4522e871399cd717abdd847ab11fe88"
236 );
237 assert_eq!(event.tick_lower, -139767);
238 assert_eq!(event.tick_upper, -139764);
239 let expected_amount = u128::from_str_radix("2bc997c8ea58d3a8521ec", 16).unwrap();
240 assert_eq!(event.amount, expected_amount);
241 assert_eq!(event.amount0, U256::ZERO);
242 let expected_amount1 = U256::from_str_radix("65b33ff853688eb", 16).unwrap();
243 assert_eq!(event.amount1, expected_amount1);
244 assert_eq!(event.block_number, 391799940);
245 }
246
247 #[rstest]
248 fn test_parse_burn_event_rpc(rpc_log: RpcLog) {
249 let dex = arbitrum::UNISWAP_V3.dex.clone();
250 let event = parse_burn_event_rpc(dex, &rpc_log).unwrap();
251
252 assert_eq!(
253 event.pool_identifier.to_string(),
254 "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
255 );
256 assert_eq!(
257 event.owner.to_string().to_lowercase(),
258 "0xc36442b4a4522e871399cd717abdd847ab11fe88"
259 );
260 assert_eq!(event.tick_lower, -139767);
261 assert_eq!(event.tick_upper, -139764);
262 let expected_amount = u128::from_str_radix("2bc997c8ea58d3a8521ec", 16).unwrap();
263 assert_eq!(event.amount, expected_amount);
264 assert_eq!(event.amount0, U256::ZERO);
265 let expected_amount1 = U256::from_str_radix("65b33ff853688eb", 16).unwrap();
266 assert_eq!(event.amount1, expected_amount1);
267 assert_eq!(event.block_number, 391799940);
268 }
269
270 #[rstest]
271 fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
272 let dex = arbitrum::UNISWAP_V3.dex.clone();
273 let event_hypersync = parse_burn_event_hypersync(dex.clone(), hypersync_log).unwrap();
274 let event_rpc = parse_burn_event_rpc(dex, &rpc_log).unwrap();
275
276 assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
277 assert_eq!(event_hypersync.owner, event_rpc.owner);
278 assert_eq!(event_hypersync.tick_lower, event_rpc.tick_lower);
279 assert_eq!(event_hypersync.tick_upper, event_rpc.tick_upper);
280 assert_eq!(event_hypersync.amount, event_rpc.amount);
281 assert_eq!(event_hypersync.amount0, event_rpc.amount0);
282 assert_eq!(event_hypersync.amount1, event_rpc.amount1);
283 assert_eq!(event_hypersync.block_number, event_rpc.block_number);
284 assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
285 }
286}