nautilus_blockchain/exchanges/parsing/uniswap_v3/
flash.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::flash::FlashEvent,
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
32// Placeholder hash - will be calculated properly later
33const FLASH_EVENT_SIGNATURE_HASH: &str =
34    "bdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633";
35
36// Define sol macro for easier parsing of Flash event data
37// event Flash(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1, uint256 paid0, uint256 paid1)
38sol! {
39    struct FlashEventData {
40        uint256 amount0;
41        uint256 amount1;
42        uint256 paid0;
43        uint256 paid1;
44    }
45}
46
47/// Parses a flash event from a Uniswap V3 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_flash_event_hypersync(
57    dex: SharedDex,
58    log: HypersyncLog,
59) -> anyhow::Result<FlashEvent> {
60    validate_event_signature_hash("FlashEvent", FLASH_EVENT_SIGNATURE_HASH, &log)?;
61
62    let sender = extract_address_from_topic(&log, 1, "sender")?;
63    let recipient = extract_address_from_topic(&log, 2, "recipient")?;
64
65    if let Some(data) = &log.data {
66        let data_bytes = data.as_ref();
67
68        // Validate if data contains 4 parameters of 32 bytes each
69        if data_bytes.len() < 4 * 32 {
70            anyhow::bail!("Flash event data is too short");
71        }
72
73        // Decode the data using the FlashEventData struct
74        let decoded = match <FlashEventData as SolType>::abi_decode(data_bytes) {
75            Ok(decoded) => decoded,
76            Err(e) => anyhow::bail!("Failed to decode flash event data: {e}"),
77        };
78
79        let pool_address = Address::from_slice(
80            log.address
81                .clone()
82                .expect("Contract address should be set in logs")
83                .as_ref(),
84        );
85        let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
86
87        Ok(FlashEvent::new(
88            dex,
89            pool_identifier,
90            extract_block_number(&log)?,
91            extract_transaction_hash(&log)?,
92            extract_transaction_index(&log)?,
93            extract_log_index(&log)?,
94            sender,
95            recipient,
96            decoded.amount0,
97            decoded.amount1,
98            decoded.paid0,
99            decoded.paid1,
100        ))
101    } else {
102        anyhow::bail!("Missing data in flash event log");
103    }
104}
105
106/// Parses a flash event from an RPC log.
107///
108/// # Errors
109///
110/// Returns an error if the log parsing fails or if the event data is invalid.
111pub fn parse_flash_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<FlashEvent> {
112    rpc_helpers::validate_event_signature(log, FLASH_EVENT_SIGNATURE_HASH, "Flash")?;
113
114    let sender = rpc_helpers::extract_address_from_topic(log, 1, "sender")?;
115    let recipient = rpc_helpers::extract_address_from_topic(log, 2, "recipient")?;
116
117    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
118
119    // Validate if data contains 4 parameters of 32 bytes each
120    if data_bytes.len() < 4 * 32 {
121        anyhow::bail!("Flash event data is too short");
122    }
123
124    // Decode the data using the FlashEventData struct
125    let decoded = match <FlashEventData as SolType>::abi_decode(&data_bytes) {
126        Ok(decoded) => decoded,
127        Err(e) => anyhow::bail!("Failed to decode flash event data: {e}"),
128    };
129
130    let pool_address = rpc_helpers::extract_address(log)?;
131    let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
132    Ok(FlashEvent::new(
133        dex,
134        pool_identifier,
135        rpc_helpers::extract_block_number(log)?,
136        rpc_helpers::extract_transaction_hash(log)?,
137        rpc_helpers::extract_transaction_index(log)?,
138        rpc_helpers::extract_log_index(log)?,
139        sender,
140        recipient,
141        decoded.amount0,
142        decoded.amount1,
143        decoded.paid0,
144        decoded.paid1,
145    ))
146}
147
148#[cfg(test)]
149mod tests {
150    use alloy::primitives::U256;
151    use rstest::*;
152    use serde_json::json;
153
154    use super::*;
155    use crate::exchanges::arbitrum;
156
157    /// Real HyperSync log from Arbitrum Flash event at block 0xfe9d5ce (266982862)
158    /// Pool: 0x4cef551255ec96d89fec975446301b5c4e164c59
159    /// sender: 0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b
160    /// recipient: 0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b
161    #[fixture]
162    fn hypersync_log() -> HypersyncLog {
163        let log_json = json!({
164            "removed": null,
165            "log_index": "0x3b",
166            "transaction_index": "0x4",
167            "transaction_hash": "0x4d345a8cae1e39654904bb7ca04e552b0fc8728ed68a28563ea4b151b96262aa",
168            "block_hash": null,
169            "block_number": "0xfe9d5ce",
170            "address": "0x4CEf551255EC96d89feC975446301b5C4e164C59",
171            "data": "0x00000000000000000000000000000000000000000000002c55804c34816b99060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220c6ab9806365120000000000000000000000000000000000000000000000000000000000000000",
172            "topics": [
173                "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633",
174                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b",
175                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
176            ]
177        });
178        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
179    }
180
181    /// Real RPC log from Arbitrum Flash event at block 0xfe9d5ce (266982862)
182    #[fixture]
183    fn rpc_log() -> RpcLog {
184        let log_json = json!({
185            "removed": false,
186            "logIndex": "0x3b",
187            "transactionIndex": "0x4",
188            "transactionHash": "0x4d345a8cae1e39654904bb7ca04e552b0fc8728ed68a28563ea4b151b96262aa",
189            "blockHash": "0xf10a01cbc75fccad0384a7447f37f06bfb01fbd08d7541a6e5f558ff9bc31ea4",
190            "blockNumber": "0xfe9d5ce",
191            "address": "0x4CEf551255EC96d89feC975446301b5C4e164C59",
192            "data": "0x00000000000000000000000000000000000000000000002c55804c34816b99060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220c6ab9806365120000000000000000000000000000000000000000000000000000000000000000",
193            "topics": [
194                "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633",
195                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b",
196                "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
197            ]
198        });
199        serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
200    }
201
202    #[rstest]
203    fn test_parse_flash_event_hypersync(hypersync_log: HypersyncLog) {
204        let dex = arbitrum::UNISWAP_V3.dex.clone();
205        let event = parse_flash_event_hypersync(dex, hypersync_log).unwrap();
206
207        assert_eq!(
208            event.pool_identifier.to_string(),
209            "0x4CEf551255EC96d89feC975446301b5C4e164C59"
210        );
211        assert_eq!(
212            event.sender.to_string().to_lowercase(),
213            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
214        );
215        assert_eq!(
216            event.recipient.to_string().to_lowercase(),
217            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
218        );
219        let expected_amount0 = U256::from_str_radix("2c55804c34816b9906", 16).unwrap();
220        assert_eq!(event.amount0, expected_amount0);
221        assert_eq!(event.amount1, U256::ZERO);
222        let expected_paid0 = U256::from_str_radix("220c6ab980636512", 16).unwrap();
223        assert_eq!(event.paid0, expected_paid0);
224        assert_eq!(event.paid1, U256::ZERO);
225        assert_eq!(event.block_number, 266982862);
226    }
227
228    #[rstest]
229    fn test_parse_flash_event_rpc(rpc_log: RpcLog) {
230        let dex = arbitrum::UNISWAP_V3.dex.clone();
231        let event = parse_flash_event_rpc(dex, &rpc_log).unwrap();
232
233        assert_eq!(
234            event.pool_identifier.to_string(),
235            "0x4CEf551255EC96d89feC975446301b5C4e164C59"
236        );
237        assert_eq!(
238            event.sender.to_string().to_lowercase(),
239            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
240        );
241        assert_eq!(
242            event.recipient.to_string().to_lowercase(),
243            "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
244        );
245        let expected_amount0 = U256::from_str_radix("2c55804c34816b9906", 16).unwrap();
246        assert_eq!(event.amount0, expected_amount0);
247        assert_eq!(event.amount1, U256::ZERO);
248        let expected_paid0 = U256::from_str_radix("220c6ab980636512", 16).unwrap();
249        assert_eq!(event.paid0, expected_paid0);
250        assert_eq!(event.paid1, U256::ZERO);
251        assert_eq!(event.block_number, 266982862);
252    }
253
254    #[rstest]
255    fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
256        let dex = arbitrum::UNISWAP_V3.dex.clone();
257        let event_hypersync = parse_flash_event_hypersync(dex.clone(), hypersync_log).unwrap();
258        let event_rpc = parse_flash_event_rpc(dex, &rpc_log).unwrap();
259
260        assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
261        assert_eq!(event_hypersync.sender, event_rpc.sender);
262        assert_eq!(event_hypersync.recipient, event_rpc.recipient);
263        assert_eq!(event_hypersync.amount0, event_rpc.amount0);
264        assert_eq!(event_hypersync.amount1, event_rpc.amount1);
265        assert_eq!(event_hypersync.paid0, event_rpc.paid0);
266        assert_eq!(event_hypersync.paid1, event_rpc.paid1);
267        assert_eq!(event_hypersync.block_number, event_rpc.block_number);
268        assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
269    }
270}