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