nautilus_blockchain/exchanges/parsing/uniswap_v3/
flash.rs1use 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 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 FLASH_EVENT_SIGNATURE_HASH: &str =
34 "bdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633";
35
36sol! {
39 struct FlashEventData {
40 uint256 amount0;
41 uint256 amount1;
42 uint256 paid0;
43 uint256 paid1;
44 }
45}
46
47pub 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 if data_bytes.len() < 4 * 32 {
70 anyhow::bail!("Flash event data is too short");
71 }
72
73 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
106pub fn parse_flash_event_rpc(dex: SharedDex, log: &RpcLog) -> anyhow::Result<FlashEvent> {
112 rpc_log::validate_event_signature(log, FLASH_EVENT_SIGNATURE_HASH, "Flash")?;
113
114 let sender = rpc_log::extract_address_from_topic(log, 1, "sender")?;
115 let recipient = rpc_log::extract_address_from_topic(log, 2, "recipient")?;
116
117 let data_bytes = rpc_log::extract_data_bytes(log)?;
118
119 if data_bytes.len() < 4 * 32 {
121 anyhow::bail!("Flash event data is too short");
122 }
123
124 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_log::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_log::extract_block_number(log)?,
136 rpc_log::extract_transaction_hash(log)?,
137 rpc_log::extract_transaction_index(log)?,
138 rpc_log::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 std::sync::Arc;
151
152 use alloy::primitives::U256;
153 use rstest::*;
154 use serde_json::json;
155
156 use super::*;
157 use crate::exchanges::arbitrum;
158
159 #[fixture]
164 fn hypersync_log() -> HypersyncLog {
165 let log_json = json!({
166 "removed": null,
167 "log_index": "0x3b",
168 "transaction_index": "0x4",
169 "transaction_hash": "0x4d345a8cae1e39654904bb7ca04e552b0fc8728ed68a28563ea4b151b96262aa",
170 "block_hash": null,
171 "block_number": "0xfe9d5ce",
172 "address": "0x4CEf551255EC96d89feC975446301b5C4e164C59",
173 "data": "0x00000000000000000000000000000000000000000000002c55804c34816b99060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220c6ab9806365120000000000000000000000000000000000000000000000000000000000000000",
174 "topics": [
175 "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633",
176 "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b",
177 "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
178 ]
179 });
180 serde_json::from_value(log_json).expect("Failed to deserialize HyperSync log")
181 }
182
183 #[fixture]
185 fn rpc_log() -> RpcLog {
186 let log_json = json!({
187 "removed": false,
188 "logIndex": "0x3b",
189 "transactionIndex": "0x4",
190 "transactionHash": "0x4d345a8cae1e39654904bb7ca04e552b0fc8728ed68a28563ea4b151b96262aa",
191 "blockHash": "0xf10a01cbc75fccad0384a7447f37f06bfb01fbd08d7541a6e5f558ff9bc31ea4",
192 "blockNumber": "0xfe9d5ce",
193 "address": "0x4CEf551255EC96d89feC975446301b5C4e164C59",
194 "data": "0x00000000000000000000000000000000000000000000002c55804c34816b99060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220c6ab9806365120000000000000000000000000000000000000000000000000000000000000000",
195 "topics": [
196 "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633",
197 "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b",
198 "0x000000000000000000000000f3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
199 ]
200 });
201 serde_json::from_value(log_json).expect("Failed to deserialize RPC log")
202 }
203
204 #[rstest]
205 fn test_parse_flash_event_hypersync(hypersync_log: HypersyncLog) {
206 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
207 let event = parse_flash_event_hypersync(dex, &hypersync_log).unwrap();
208
209 assert_eq!(
210 event.pool_identifier.to_string(),
211 "0x4CEf551255EC96d89feC975446301b5C4e164C59"
212 );
213 assert_eq!(
214 event.sender.to_string().to_lowercase(),
215 "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
216 );
217 assert_eq!(
218 event.recipient.to_string().to_lowercase(),
219 "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
220 );
221 let expected_amount0 = U256::from_str_radix("2c55804c34816b9906", 16).unwrap();
222 assert_eq!(event.amount0, expected_amount0);
223 assert_eq!(event.amount1, U256::ZERO);
224 let expected_paid0 = U256::from_str_radix("220c6ab980636512", 16).unwrap();
225 assert_eq!(event.paid0, expected_paid0);
226 assert_eq!(event.paid1, U256::ZERO);
227 assert_eq!(event.block_number, 266982862);
228 }
229
230 #[rstest]
231 fn test_parse_flash_event_rpc(rpc_log: RpcLog) {
232 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
233 let event = parse_flash_event_rpc(dex, &rpc_log).unwrap();
234
235 assert_eq!(
236 event.pool_identifier.to_string(),
237 "0x4CEf551255EC96d89feC975446301b5C4e164C59"
238 );
239 assert_eq!(
240 event.sender.to_string().to_lowercase(),
241 "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
242 );
243 assert_eq!(
244 event.recipient.to_string().to_lowercase(),
245 "0xf3f521ee74debaa28fd0ea1e8ca2fd8d6c110d8b"
246 );
247 let expected_amount0 = U256::from_str_radix("2c55804c34816b9906", 16).unwrap();
248 assert_eq!(event.amount0, expected_amount0);
249 assert_eq!(event.amount1, U256::ZERO);
250 let expected_paid0 = U256::from_str_radix("220c6ab980636512", 16).unwrap();
251 assert_eq!(event.paid0, expected_paid0);
252 assert_eq!(event.paid1, U256::ZERO);
253 assert_eq!(event.block_number, 266982862);
254 }
255
256 #[rstest]
257 fn test_hypersync_rpc_match(hypersync_log: HypersyncLog, rpc_log: RpcLog) {
258 let dex = Arc::clone(&arbitrum::UNISWAP_V3.dex);
259 let event_hypersync =
260 parse_flash_event_hypersync(Arc::clone(&dex), &hypersync_log).unwrap();
261 let event_rpc = parse_flash_event_rpc(dex, &rpc_log).unwrap();
262
263 assert_eq!(event_hypersync.pool_identifier, event_rpc.pool_identifier);
264 assert_eq!(event_hypersync.sender, event_rpc.sender);
265 assert_eq!(event_hypersync.recipient, event_rpc.recipient);
266 assert_eq!(event_hypersync.amount0, event_rpc.amount0);
267 assert_eq!(event_hypersync.amount1, event_rpc.amount1);
268 assert_eq!(event_hypersync.paid0, event_rpc.paid0);
269 assert_eq!(event_hypersync.paid1, event_rpc.paid1);
270 assert_eq!(event_hypersync.block_number, event_rpc.block_number);
271 assert_eq!(event_hypersync.transaction_hash, event_rpc.transaction_hash);
272 }
273}