nautilus_blockchain/exchanges/parsing/uniswap_v3/
mint.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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
35// Define sol macro for easier parsing of Mint event data
36// It contains 4 parameters of 32 bytes each:
37// sender (address), amount (uint128), amount0 (uint256), amount1 (uint256)
38sol! {
39    struct MintEventData {
40        address sender;
41        uint128 amount;
42        uint256 amount0;
43        uint256 amount1;
44    }
45}
46
47/// Parses a mint event from a HyperSync log.
48///
49/// # Errors
50///
51/// Returns an error if the log parsing fails or if the event data is invalid.
52///
53/// # Panics
54///
55/// Panics if the contract address is not set in the log.
56pub 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    // Extract int24 tickLower from topic2 (stored as a 32-byte padded value)
62    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    // Extract int24 tickUpper from topic3 (stored as a 32-byte padded value)
71    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        // Validate if data contains 4 parameters of 32 bytes each
83        if data_bytes.len() < 4 * 32 {
84            anyhow::bail!("Mint event data is too short");
85        }
86
87        // Decode the data using the MintEventData struct
88        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
120/// Parses a mint event from an RPC log.
121///
122/// # Errors
123///
124/// Returns an error if the log parsing fails or if the event data is invalid.
125pub 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    // Extract int24 tickLower from topic2 (stored as a 32-byte padded value)
131    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    // Extract int24 tickUpper from topic3 (stored as a 32-byte padded value)
135    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    // Validate if data contains 4 parameters of 32 bytes each
141    if data_bytes.len() < 4 * 32 {
142        anyhow::bail!("Mint event data is too short");
143    }
144
145    // Decode the data using the MintEventData struct
146    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    /// Real HyperSync log from Arbitrum Mint event at block 391053023
180    /// Pool: 0xd13040d4fe917ee704158cfcb3338dcd2838b245
181    /// owner: 0xc36442b4a4522e871399cd717abdd847ab11fe88 (NonfungiblePositionManager)
182    /// tickLower: -887272 (0xfff27618)
183    /// tickUpper: 887272 (0x0d89e8)
184    #[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    /// Real RPC log from Arbitrum Mint event at block 391053023
206    #[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        // Both parsers should produce identical results
282        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}