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