Skip to main content

nautilus_blockchain/exchanges/parsing/uniswap_v3/
collect.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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::collect::CollectEvent,
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 COLLECT_EVENT_SIGNATURE_HASH: &str =
33    "70935338e69775456a85ddef226c395fb668b63fa0115f5f20610b388e6ca9c0";
34
35// Define sol macro for easier parsing of Collect event data
36// It contains 3 parameters of 32 bytes each:
37// recipient (address), amount0 (uint128), amount1 (uint128)
38sol! {
39    struct CollectEventData {
40        address recipient;
41        uint128 amount0;
42        uint128 amount1;
43    }
44}
45
46/// Parses a collect event from a Uniswap V3 log.
47///
48/// # Errors
49///
50/// Returns an error if the log parsing fails or if the event data is invalid.
51///
52/// # Panics
53///
54/// Panics if the contract address is not set in the log.
55pub fn parse_collect_event_hypersync(
56    dex: SharedDex,
57    log: &HypersyncLog,
58) -> anyhow::Result<CollectEvent> {
59    validate_event_signature_hash("Collect", COLLECT_EVENT_SIGNATURE_HASH, log)?;
60
61    let owner = extract_address_from_topic(log, 1, "owner")?;
62
63    // Extract int24 tickLower from topic2 (stored as a 32-byte padded value)
64    let tick_lower = match log.topics.get(2).and_then(|t| t.as_ref()) {
65        Some(topic) => {
66            let tick_lower_bytes: [u8; 32] = topic.as_ref().try_into()?;
67            i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?)
68        }
69        None => anyhow::bail!("Missing tickLower in topic2 when parsing collect event"),
70    };
71
72    // Extract int24 tickUpper from topic3 (stored as a 32-byte padded value)
73    let tick_upper = match log.topics.get(3).and_then(|t| t.as_ref()) {
74        Some(topic) => {
75            let tick_upper_bytes: [u8; 32] = topic.as_ref().try_into()?;
76            i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?)
77        }
78        None => anyhow::bail!("Missing tickUpper in topic3 when parsing collect event"),
79    };
80
81    if let Some(data) = &log.data {
82        let data_bytes = data.as_ref();
83
84        // Validate if data contains 3 parameters of 32 bytes each
85        if data_bytes.len() < 3 * 32 {
86            anyhow::bail!("Collect event data is too short");
87        }
88
89        // Decode the data using the CollectEventData struct
90        let decoded = match <CollectEventData as SolType>::abi_decode(data_bytes) {
91            Ok(decoded) => decoded,
92            Err(e) => anyhow::bail!("Failed to decode collect event data: {e}"),
93        };
94
95        let pool_address = Address::from_slice(
96            log.address
97                .clone()
98                .expect("Contract address should be set in logs")
99                .as_ref(),
100        );
101        let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
102        Ok(CollectEvent::new(
103            dex,
104            pool_identifier,
105            extract_block_number(log)?,
106            extract_transaction_hash(log)?,
107            extract_transaction_index(log)?,
108            extract_log_index(log)?,
109            owner,
110            decoded.recipient,
111            tick_lower,
112            tick_upper,
113            decoded.amount0,
114            decoded.amount1,
115        ))
116    } else {
117        Err(anyhow::anyhow!("Missing data in collect event log"))
118    }
119}
120
121/// Parses a collect event from an RPC log.
122///
123/// # Errors
124///
125/// Returns an error if the log parsing fails or if the event data is invalid.
126pub fn parse_collect_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<CollectEvent> {
127    rpc_log::validate_event_signature(log, COLLECT_EVENT_SIGNATURE_HASH, "Collect")?;
128
129    let owner = rpc_log::extract_address_from_topic(log, 1, "owner")?;
130
131    // Extract int24 tickLower from topic2 (stored as a 32-byte padded value)
132    let tick_lower_bytes = rpc_log::extract_topic_bytes(log, 2)?;
133    let tick_lower = i32::from_be_bytes(tick_lower_bytes[28..32].try_into()?);
134
135    // Extract int24 tickUpper from topic3 (stored as a 32-byte padded value)
136    let tick_upper_bytes = rpc_log::extract_topic_bytes(log, 3)?;
137    let tick_upper = i32::from_be_bytes(tick_upper_bytes[28..32].try_into()?);
138
139    let data_bytes = rpc_log::extract_data_bytes(log)?;
140
141    // Validate if data contains 3 parameters of 32 bytes each
142    if data_bytes.len() < 3 * 32 {
143        anyhow::bail!("Collect event data is too short");
144    }
145
146    // Decode the data using the CollectEventData struct
147    let decoded = match <CollectEventData as SolType>::abi_decode(&data_bytes) {
148        Ok(decoded) => decoded,
149        Err(e) => anyhow::bail!("Failed to decode collect event data: {e}"),
150    };
151
152    let pool_address = rpc_log::extract_address(log)?;
153    let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
154    Ok(CollectEvent::new(
155        dex,
156        pool_identifier,
157        rpc_log::extract_block_number(log)?,
158        rpc_log::extract_transaction_hash(log)?,
159        rpc_log::extract_transaction_index(log)?,
160        rpc_log::extract_log_index(log)?,
161        owner,
162        decoded.recipient,
163        tick_lower,
164        tick_upper,
165        decoded.amount0,
166        decoded.amount1,
167    ))
168}
169
170#[cfg(test)]
171mod tests {
172    use std::sync::Arc;
173
174    use rstest::*;
175    use serde_json::json;
176
177    use super::*;
178    use crate::exchanges::arbitrum;
179
180    /// Real HyperSync log from Arbitrum Collect event at block 0x175a6484 (391799940)
181    /// Pool: 0xd13040d4fe917ee704158cfcb3338dcd2838b245
182    /// owner: 0xc36442b4a4522e871399cd717abdd847ab11fe88 (NonfungiblePositionManager)
183    /// recipient: 0xa61da382c18d9d5beb905ea192bae25e4c15d512
184    /// tickLower: -139767 (0xfffdde09)
185    /// tickUpper: -139764 (0xfffdde0c)
186    #[fixture]
187    fn hypersync_log() -> HypersyncLog {
188        let log_json = json!({
189            "removed": null,
190            "log_index": "0x11",
191            "transaction_index": "0x5",
192            "transaction_hash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
193            "block_hash": null,
194            "block_number": "0x175a6484",
195            "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
196            "data": "0x000000000000000000000000a61da382c18d9d5beb905ea192bae25e4c15d5120000000000000000000000000000000000000000000000bf28af828dd3ae56340000000000000000000000000000000000000000000000000665eae21b1cffc8",
197            "topics": [
198                "0x70935338e69775456a85ddef226c395fb668b63fa0115f5f20610b388e6ca9c0",
199                "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
200                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
201                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
202            ]
203        });
204        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
205    }
206
207    /// Real RPC log from Arbitrum Collect event at block 0x175a6484 (391799940)
208    #[fixture]
209    fn rpc_log() -> RpcLog {
210        let log_json = json!({
211            "removed": false,
212            "logIndex": "0x11",
213            "transactionIndex": "0x5",
214            "transactionHash": "0x0c70f6d6bcf8508ba620b9d1250c95ad67108e35707c5d7456349ea207051bae",
215            "blockHash": "0xe925eaa1f5178ceedfa24043a974edb928ddab7195600b6b99ff5403fbf13c8b",
216            "blockNumber": "0x175a6484",
217            "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
218            "data": "0x000000000000000000000000a61da382c18d9d5beb905ea192bae25e4c15d5120000000000000000000000000000000000000000000000bf28af828dd3ae56340000000000000000000000000000000000000000000000000665eae21b1cffc8",
219            "topics": [
220                "0x70935338e69775456a85ddef226c395fb668b63fa0115f5f20610b388e6ca9c0",
221                "0x000000000000000000000000c36442b4a4522e871399cd717abdd847ab11fe88",
222                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde09",
223                "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdde0c"
224            ]
225        });
226        serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
227    }
228
229    #[rstest]
230    fn test_parse_collect_event_hypersync(hypersync_log: HypersyncLog) {
231        let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
232        let event = parse_collect_event_hypersync(dex, &hypersync_log).unwrap();
233
234        assert_eq!(
235            event.pool_identifier.to_string(),
236            "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
237        );
238        assert_eq!(
239            event.owner.to_string().to_lowercase(),
240            "0xc36442b4a4522e871399cd717abdd847ab11fe88"
241        );
242        assert_eq!(
243            event.recipient.to_string().to_lowercase(),
244            "0xa61da382c18d9d5beb905ea192bae25e4c15d512"
245        );
246        assert_eq!(event.tick_lower, -139767);
247        assert_eq!(event.tick_upper, -139764);
248        let expected_amount0 = u128::from_str_radix("bf28af828dd3ae5634", 16).unwrap();
249        assert_eq!(event.amount0, expected_amount0);
250        let expected_amount1 = u128::from_str_radix("665eae21b1cffc8", 16).unwrap();
251        assert_eq!(event.amount1, expected_amount1);
252        assert_eq!(event.block_number, 391799940);
253    }
254
255    #[rstest]
256    fn test_parse_collect_event_rpc(rpc_log: RpcLog) {
257        let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
258        let event = parse_collect_event_rpc(dex, &rpc_log).unwrap();
259
260        assert_eq!(
261            event.pool_identifier.to_string(),
262            "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
263        );
264        assert_eq!(
265            event.owner.to_string().to_lowercase(),
266            "0xc36442b4a4522e871399cd717abdd847ab11fe88"
267        );
268        assert_eq!(
269            event.recipient.to_string().to_lowercase(),
270            "0xa61da382c18d9d5beb905ea192bae25e4c15d512"
271        );
272        assert_eq!(event.tick_lower, -139767);
273        assert_eq!(event.tick_upper, -139764);
274        let expected_amount0 = u128::from_str_radix("bf28af828dd3ae5634", 16).unwrap();
275        assert_eq!(event.amount0, expected_amount0);
276        let expected_amount1 = u128::from_str_radix("665eae21b1cffc8", 16).unwrap();
277        assert_eq!(event.amount1, expected_amount1);
278        assert_eq!(event.block_number, 391799940);
279    }
280
281    #[rstest]
282    fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
283        let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
284        let event_hypersync =
285            parse_collect_event_hypersync(Arc::clone(&dex), &hypersync_log).unwrap();
286        let event_rpc = parse_collect_event_rpc(dex, &rpc_log).unwrap();
287
288        assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
289        assert_eq!(event_hypersync.owner, event_rpc.owner);
290        assert_eq!(event_hypersync.recipient, event_rpc.recipient);
291        assert_eq!(event_hypersync.tick_lower, event_rpc.tick_lower);
292        assert_eq!(event_hypersync.tick_upper, event_rpc.tick_upper);
293        assert_eq!(event_hypersync.amount0, event_rpc.amount0);
294        assert_eq!(event_hypersync.amount1, event_rpc.amount1);
295        assert_eq!(event_hypersync.block_number, event_rpc.block_number);
296        assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
297    }
298}