nautilus_blockchain/rpc/
log.rs1use alloy::primitives::Address;
21use nautilus_core::hex;
22use nautilus_model::defi::rpc::RpcLog;
23
24pub fn decode_hex(hex: &str) -> anyhow::Result<Vec<u8>> {
30 hex::decode(hex.trim_start_matches("0x")).map_err(|e| anyhow::anyhow!("Invalid hex: {e}"))
31}
32
33pub fn parse_hex_u64(hex: &str) -> anyhow::Result<u64> {
39 u64::from_str_radix(hex.trim_start_matches("0x"), 16)
40 .map_err(|e| anyhow::anyhow!("Invalid hex u64: {e}"))
41}
42
43pub fn parse_hex_u32(hex: &str) -> anyhow::Result<u32> {
49 u32::from_str_radix(hex.trim_start_matches("0x"), 16)
50 .map_err(|e| anyhow::anyhow!("Invalid hex u32: {e}"))
51}
52
53pub fn extract_block_number(log: &RpcLog) -> anyhow::Result<u64> {
59 let hex = log
60 .block_number
61 .as_ref()
62 .ok_or_else(|| anyhow::anyhow!("Missing block number"))?;
63 parse_hex_u64(hex)
64}
65
66pub fn extract_transaction_hash(log: &RpcLog) -> anyhow::Result<String> {
72 log.transaction_hash
73 .clone()
74 .ok_or_else(|| anyhow::anyhow!("Missing transaction hash"))
75}
76
77pub fn extract_transaction_index(log: &RpcLog) -> anyhow::Result<u32> {
83 let hex = log
84 .transaction_index
85 .as_ref()
86 .ok_or_else(|| anyhow::anyhow!("Missing transaction index"))?;
87 parse_hex_u32(hex)
88}
89
90pub fn extract_log_index(log: &RpcLog) -> anyhow::Result<u32> {
96 let hex = log
97 .log_index
98 .as_ref()
99 .ok_or_else(|| anyhow::anyhow!("Missing log index"))?;
100 parse_hex_u32(hex)
101}
102
103pub fn extract_address(log: &RpcLog) -> anyhow::Result<Address> {
109 let bytes = decode_hex(&log.address)?;
110 anyhow::ensure!(
111 bytes.len() == Address::len_bytes(),
112 "Invalid contract address length: expected {} bytes, was {}",
113 Address::len_bytes(),
114 bytes.len()
115 );
116 Ok(Address::from_slice(&bytes))
117}
118
119pub fn extract_topic_bytes(log: &RpcLog, index: usize) -> anyhow::Result<Vec<u8>> {
125 let hex = log
126 .topics
127 .get(index)
128 .ok_or_else(|| anyhow::anyhow!("Missing topic at index {index}"))?;
129 decode_hex(hex)
130}
131
132pub fn extract_address_from_topic(
141 log: &RpcLog,
142 index: usize,
143 description: &str,
144) -> anyhow::Result<Address> {
145 let bytes = extract_topic_bytes(log, index)
146 .map_err(|_| anyhow::anyhow!("Missing {description} address in topic{index}"))?;
147 anyhow::ensure!(
148 bytes.len() >= 32,
149 "Topic must be at least 32 bytes, was {}",
150 bytes.len()
151 );
152 Ok(Address::from_slice(&bytes[12..32]))
153}
154
155pub fn extract_data_bytes(log: &RpcLog) -> anyhow::Result<Vec<u8>> {
161 decode_hex(&log.data)
162}
163
164pub fn validate_event_signature(
174 log: &RpcLog,
175 expected_hash: &str,
176 event_name: &str,
177) -> anyhow::Result<()> {
178 let sig_bytes = extract_topic_bytes(log, 0)?;
179 let actual_hex = hex::encode(&sig_bytes);
180 anyhow::ensure!(
181 actual_hex == expected_hash,
182 "Invalid event signature for '{event_name}': expected {expected_hash}, was {actual_hex}",
183 );
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use rstest::{fixture, rstest};
190
191 use super::*;
192
193 #[fixture]
199 fn log() -> RpcLog {
200 RpcLog {
201 removed: false,
202 log_index: Some("0x0".to_string()),
203 transaction_index: Some("0x0".to_string()),
204 transaction_hash: Some(
205 "0x24058dde7caf5b8b70041de8b27731f20f927365f210247c3e720e947b9098e7".to_string(),
206 ),
207 block_hash: Some(
208 "0xd371b6c7b04ec33d6470f067a82e87d7b294b952bea7a46d7b939b4c7addc275".to_string(),
209 ),
210 block_number: Some("0xb9".to_string()),
211 address: "0x1f98431c8ad98523631ae4a59f267346ea31f984".to_string(),
212 data: "0x000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000b9fc136980d98c034a529aadbd5651c087365d5f".to_string(),
213 topics: vec![
214 "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118".to_string(),
215 "0x0000000000000000000000002e5353426c89f4ecd52d1036da822d47e73376c4".to_string(),
216 "0x000000000000000000000000838930cfe7502dd36b0b1ebbef8001fbf94f3bfb".to_string(),
217 "0x0000000000000000000000000000000000000000000000000000000000000bb8".to_string(),
218 ],
219 }
220 }
221
222 #[rstest]
223 fn test_decode_hex_with_prefix() {
224 let result = decode_hex("0x1234").unwrap();
225 assert_eq!(result, vec![0x12, 0x34]);
226 }
227
228 #[rstest]
229 fn test_decode_hex_without_prefix() {
230 let result = decode_hex("1234").unwrap();
231 assert_eq!(result, vec![0x12, 0x34]);
232 }
233
234 #[rstest]
235 fn test_parse_hex_u64_block_185() {
236 assert_eq!(parse_hex_u64("0xb9").unwrap(), 185);
238 assert_eq!(parse_hex_u64("b9").unwrap(), 185);
239 }
240
241 #[rstest]
242 fn test_parse_hex_u32() {
243 assert_eq!(parse_hex_u32("0x0").unwrap(), 0);
244 assert_eq!(parse_hex_u32("0xbb8").unwrap(), 3000); }
246
247 #[rstest]
248 fn test_extract_block_number(log: RpcLog) {
249 assert_eq!(extract_block_number(&log).unwrap(), 185);
250 }
251
252 #[rstest]
253 fn test_extract_transaction_hash(log: RpcLog) {
254 assert_eq!(
255 extract_transaction_hash(&log).unwrap(),
256 "0x24058dde7caf5b8b70041de8b27731f20f927365f210247c3e720e947b9098e7"
257 );
258 }
259
260 #[rstest]
261 fn test_extract_transaction_index(log: RpcLog) {
262 assert_eq!(extract_transaction_index(&log).unwrap(), 0);
263 }
264
265 #[rstest]
266 fn test_extract_log_index(log: RpcLog) {
267 assert_eq!(extract_log_index(&log).unwrap(), 0);
268 }
269
270 #[rstest]
271 fn test_extract_address(log: RpcLog) {
272 let address = extract_address(&log).unwrap();
273 assert_eq!(
275 address.to_string().to_lowercase(),
276 "0x1f98431c8ad98523631ae4a59f267346ea31f984"
277 );
278 }
279
280 #[rstest]
281 #[case("0x", 0)]
282 #[case("0x00112233445566778899aabbccddeeff001122", 19)]
283 #[case("0x00112233445566778899aabbccddeeff0011223344", 21)]
284 fn test_extract_address_rejects_wrong_length(
285 mut log: RpcLog,
286 #[case] address: &str,
287 #[case] actual: usize,
288 ) {
289 log.address = address.to_string();
290
291 let error = extract_address(&log).unwrap_err();
292
293 assert_eq!(
294 error.to_string(),
295 format!("Invalid contract address length: expected 20 bytes, was {actual}")
296 );
297 }
298
299 #[rstest]
300 fn test_extract_address_from_topic_token0(log: RpcLog) {
301 let address = extract_address_from_topic(&log, 1, "token0").unwrap();
302 assert_eq!(
303 address.to_string().to_lowercase(),
304 "0x2e5353426c89f4ecd52d1036da822d47e73376c4"
305 );
306 }
307
308 #[rstest]
309 fn test_extract_address_from_topic_token1(log: RpcLog) {
310 let address = extract_address_from_topic(&log, 2, "token1").unwrap();
311 assert_eq!(
312 address.to_string().to_lowercase(),
313 "0x838930cfe7502dd36b0b1ebbef8001fbf94f3bfb"
314 );
315 }
316
317 #[rstest]
318 fn test_extract_data_bytes(log: RpcLog) {
319 let data = extract_data_bytes(&log).unwrap();
320 assert_eq!(data.len(), 64); assert_eq!(data[31], 0x3c);
324 }
325
326 #[rstest]
327 fn test_validate_event_signature_pool_created(log: RpcLog) {
328 let expected = "783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118";
329 assert!(validate_event_signature(&log, expected, "PoolCreated").is_ok());
330 }
331
332 #[rstest]
333 fn test_validate_event_signature_mismatch(log: RpcLog) {
334 let wrong = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
336 let result = validate_event_signature(&log, wrong, "Swap");
337 assert!(result.is_err());
338 assert!(
339 result
340 .unwrap_err()
341 .to_string()
342 .contains("Invalid event signature")
343 );
344 }
345}