nautilus_blockchain/exchanges/parsing/uniswap_v3/
swap.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::swap::SwapEvent,
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 SWAP_EVENT_SIGNATURE_HASH: &str =
33    "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
34
35// Define sol macro for easier parsing of Swap event data
36// It contains 5 parameters of 32 bytes each:
37// amount0 (int256), amount1 (int256), sqrtPriceX96 (uint160), liquidity (uint128), tick (int24)
38sol! {
39    struct SwapEventData {
40        int256 amount0;
41        int256 amount1;
42        uint160 sqrt_price_x96;
43        uint128 liquidity;
44        int24 tick;
45    }
46}
47
48/// Parses a swap event from a HyperSync log.
49///
50/// # Errors
51///
52/// Returns an error if the log parsing fails or if the event data is invalid.
53///
54/// # Panics
55///
56/// Panics if the contract address is not set in the log.
57pub fn parse_swap_event_hypersync(dex: SharedDex, log: HypersyncLog) -> anyhow::Result<SwapEvent> {
58    validate_event_signature_hash("SwapEvent", SWAP_EVENT_SIGNATURE_HASH, &log)?;
59
60    let sender = extract_address_from_topic(&log, 1, "sender")?;
61    let recipient = extract_address_from_topic(&log, 2, "recipient")?;
62
63    if let Some(data) = &log.data {
64        let data_bytes = data.as_ref();
65
66        // Validate if data contains 5 parameters of 32 bytes each
67        if data_bytes.len() < 5 * 32 {
68            anyhow::bail!("Swap event data is too short");
69        }
70
71        // Decode the data using the SwapEventData struct
72        let decoded = match <SwapEventData as SolType>::abi_decode(data_bytes) {
73            Ok(decoded) => decoded,
74            Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
75        };
76        let _ = decoded.amount0;
77        let pool_address = Address::from_slice(
78            log.address
79                .clone()
80                .expect("Contract address should be set in logs")
81                .as_ref(),
82        );
83        let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
84        Ok(SwapEvent::new(
85            dex,
86            pool_identifier,
87            extract_block_number(&log)?,
88            extract_transaction_hash(&log)?,
89            extract_transaction_index(&log)?,
90            extract_log_index(&log)?,
91            sender,
92            recipient,
93            decoded.amount0,
94            decoded.amount1,
95            decoded.sqrt_price_x96,
96            decoded.liquidity,
97            decoded.tick.as_i32(),
98        ))
99    } else {
100        Err(anyhow::anyhow!("Missing data in swap event log"))
101    }
102}
103
104/// Parses a swap event from an RPC log.
105///
106/// # Errors
107///
108/// Returns an error if the log parsing fails or if the event data is invalid.
109pub fn parse_swap_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<SwapEvent> {
110    rpc_helpers::validate_event_signature(log, SWAP_EVENT_SIGNATURE_HASH, "Swap")?;
111
112    let sender = rpc_helpers::extract_address_from_topic(log, 1, "sender")?;
113    let recipient = rpc_helpers::extract_address_from_topic(log, 2, "recipient")?;
114
115    let data_bytes = rpc_helpers::extract_data_bytes(log)?;
116
117    // Validate if data contains 5 parameters of 32 bytes each
118    if data_bytes.len() < 5 * 32 {
119        anyhow::bail!("Swap event data is too short");
120    }
121
122    // Decode the data using the SwapEventData struct
123    let decoded = match <SwapEventData as SolType>::abi_decode(&data_bytes) {
124        Ok(decoded) => decoded,
125        Err(e) => anyhow::bail!("Failed to decode swap event data: {e}"),
126    };
127
128    let pool_address = rpc_helpers::extract_address(log)?;
129    let pool_identifier = PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
130    Ok(SwapEvent::new(
131        dex,
132        pool_identifier,
133        rpc_helpers::extract_block_number(log)?,
134        rpc_helpers::extract_transaction_hash(log)?,
135        rpc_helpers::extract_transaction_index(log)?,
136        rpc_helpers::extract_log_index(log)?,
137        sender,
138        recipient,
139        decoded.amount0,
140        decoded.amount1,
141        decoded.sqrt_price_x96,
142        decoded.liquidity,
143        decoded.tick.as_i32(),
144    ))
145}
146
147#[cfg(test)]
148mod tests {
149    use alloy::primitives::{I256, U160, U256};
150    use rstest::*;
151    use serde_json::json;
152
153    use super::*;
154    use crate::exchanges::arbitrum;
155
156    /// Real HyperSync log from Arbitrum Swap event at block 0x17513444 (391197764)
157    /// Pool: 0xd13040d4fe917ee704158cfcb3338dcd2838b245
158    /// sender: 0x9da4a7d3cf502337797ea37724f7afc426377119
159    /// recipient: 0xd491076c7316bc28fd4d35e3da9ab5286d079250
160    /// amount0: negative (token out)
161    /// amount1: positive (token in)
162    /// tick: -139475 (0xfffddf2d)
163    #[fixture]
164    fn hypersync_log() -> HypersyncLog {
165        let log_json = json!({
166            "removed": null,
167            "log_index": "0x6",
168            "transaction_index": "0x3",
169            "transaction_hash": "0x381ae1c1b65bba31abdfc68ef6b3e3e49913161a15398ccff3b242b05473e720",
170            "block_hash": null,
171            "block_number": "0x17513444",
172            "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
173            "data": "0xffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe000000000000000000000000000000000000000000000000000e2a274937d6380000000000000000000000000000000000000000003d5fe159ea44896552c1cd000000000000000000000000000000000000000000000074009aac72ba0a9b1cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffddf2d",
174            "topics": [
175                "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
176                "0x0000000000000000000000009da4a7d3cf502337797ea37724f7afc426377119",
177                "0x000000000000000000000000d491076c7316bc28fd4d35e3da9ab5286d079250"
178            ]
179        });
180        serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
181    }
182
183    /// Real RPC log from Arbitrum Swap event at block 0x17513444 (391197764)
184    #[fixture]
185    fn rpc_log() -> RpcLog {
186        let log_json = json!({
187            "removed": false,
188            "logIndex": "0x6",
189            "transactionIndex": "0x3",
190            "transactionHash": "0x381ae1c1b65bba31abdfc68ef6b3e3e49913161a15398ccff3b242b05473e720",
191            "blockHash": "0x43082eabb648a3b87bd22abf7ec645a97e6e7f099dcc18894830c70d85675fae",
192            "blockNumber": "0x17513444",
193            "address": "0xd13040d4fe917EE704158CfCB3338dCd2838B245",
194            "data": "0xffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe000000000000000000000000000000000000000000000000000e2a274937d6380000000000000000000000000000000000000000003d5fe159ea44896552c1cd000000000000000000000000000000000000000000000074009aac72ba0a9b1cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffddf2d",
195            "topics": [
196                "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
197                "0x0000000000000000000000009da4a7d3cf502337797ea37724f7afc426377119",
198                "0x000000000000000000000000d491076c7316bc28fd4d35e3da9ab5286d079250"
199            ]
200        });
201        serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
202    }
203
204    #[rstest]
205    fn test_parse_swap_event_hypersync(hypersync_log: HypersyncLog) {
206        let dex = arbitrum::UNISWAP_V3.dex.clone();
207        let event = parse_swap_event_hypersync(dex, hypersync_log).unwrap();
208
209        assert_eq!(
210            event.pool_identifier.to_string(),
211            "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
212        );
213        assert_eq!(
214            event.sender.to_string().to_lowercase(),
215            "0x9da4a7d3cf502337797ea37724f7afc426377119"
216        );
217        assert_eq!(
218            event.receiver.to_string().to_lowercase(),
219            "0xd491076c7316bc28fd4d35e3da9ab5286d079250"
220        );
221        let expected_amount0 = I256::from_raw(
222            U256::from_str_radix(
223                "ffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe",
224                16,
225            )
226            .unwrap(),
227        );
228        assert_eq!(event.amount0, expected_amount0);
229        let expected_amount1 = I256::from_raw(U256::from_str_radix("0e2a274937d638", 16).unwrap());
230        assert_eq!(event.amount1, expected_amount1);
231        let expected_sqrt_price = U160::from_str_radix("3d5fe159ea44896552c1cd", 16).unwrap();
232        assert_eq!(event.sqrt_price_x96, expected_sqrt_price);
233        let expected_liquidity = u128::from_str_radix("74009aac72ba0a9b1c", 16).unwrap();
234        assert_eq!(event.liquidity, expected_liquidity);
235        assert_eq!(event.tick, -139475);
236        assert_eq!(event.block_number, 391197764);
237    }
238
239    #[rstest]
240    fn test_parse_swap_event_rpc(rpc_log: RpcLog) {
241        let dex = arbitrum::UNISWAP_V3.dex.clone();
242        let event = parse_swap_event_rpc(dex, &rpc_log).unwrap();
243
244        assert_eq!(
245            event.pool_identifier.to_string(),
246            "0xd13040d4fe917EE704158CfCB3338dCd2838B245"
247        );
248        assert_eq!(
249            event.sender.to_string().to_lowercase(),
250            "0x9da4a7d3cf502337797ea37724f7afc426377119"
251        );
252        assert_eq!(
253            event.receiver.to_string().to_lowercase(),
254            "0xd491076c7316bc28fd4d35e3da9ab5286d079250"
255        );
256        let expected_amount0 = I256::from_raw(
257            U256::from_str_radix(
258                "ffffffffffffffffffffffffffffffffffffffffffffff0918233055494456fe",
259                16,
260            )
261            .unwrap(),
262        );
263        assert_eq!(event.amount0, expected_amount0);
264        let expected_amount1 = I256::from_raw(U256::from_str_radix("0e2a274937d638", 16).unwrap());
265        assert_eq!(event.amount1, expected_amount1);
266        let expected_sqrt_price = U160::from_str_radix("3d5fe159ea44896552c1cd", 16).unwrap();
267        assert_eq!(event.sqrt_price_x96, expected_sqrt_price);
268        let expected_liquidity = u128::from_str_radix("74009aac72ba0a9b1c", 16).unwrap();
269        assert_eq!(event.liquidity, expected_liquidity);
270        assert_eq!(event.tick, -139475);
271        assert_eq!(event.block_number, 391197764);
272    }
273
274    #[rstest]
275    fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
276        let dex = arbitrum::UNISWAP_V3.dex.clone();
277        let event_hypersync = parse_swap_event_hypersync(dex.clone(), hypersync_log).unwrap();
278        let event_rpc = parse_swap_event_rpc(dex, &rpc_log).unwrap();
279
280        assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
281        assert_eq!(event_hypersync.sender, event_rpc.sender);
282        assert_eq!(event_hypersync.receiver, event_rpc.receiver);
283        assert_eq!(event_hypersync.amount0, event_rpc.amount0);
284        assert_eq!(event_hypersync.amount1, event_rpc.amount1);
285        assert_eq!(event_hypersync.sqrt_price_x96, event_rpc.sqrt_price_x96);
286        assert_eq!(event_hypersync.liquidity, event_rpc.liquidity);
287        assert_eq!(event_hypersync.tick, event_rpc.tick);
288        assert_eq!(event_hypersync.block_number, event_rpc.block_number);
289        assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
290    }
291}