nautilus_blockchain/rpc/
helpers.rs1use alloy::primitives::Address;
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 Ok(Address::from_slice(&bytes))
111}
112
113pub fn extract_topic_bytes(log: &RpcLog, index: usize) -> anyhow::Result<Vec<u8>> {
119 let hex = log
120 .topics
121 .get(index)
122 .ok_or_else(|| anyhow::anyhow!("Missing topic at index {index}"))?;
123 decode_hex(hex)
124}
125
126pub fn extract_address_from_topic(
135 log: &RpcLog,
136 index: usize,
137 description: &str,
138) -> anyhow::Result<Address> {
139 let bytes = extract_topic_bytes(log, index)
140 .map_err(|_| anyhow::anyhow!("Missing {description} address in topic{index}"))?;
141 anyhow::ensure!(
142 bytes.len() >= 32,
143 "Topic must be at least 32 bytes, got {}",
144 bytes.len()
145 );
146 Ok(Address::from_slice(&bytes[12..32]))
147}
148
149pub fn extract_data_bytes(log: &RpcLog) -> anyhow::Result<Vec<u8>> {
155 decode_hex(&log.data)
156}
157
158pub fn validate_event_signature(
168 log: &RpcLog,
169 expected_hash: &str,
170 event_name: &str,
171) -> anyhow::Result<()> {
172 let sig_bytes = extract_topic_bytes(log, 0)?;
173 let actual_hex = hex::encode(&sig_bytes);
174 anyhow::ensure!(
175 actual_hex == expected_hash,
176 "Invalid event signature for '{event_name}': expected {expected_hash}, got {actual_hex}",
177 );
178 Ok(())
179}
180
181#[cfg(test)]
182mod tests {
183 use rstest::{fixture, rstest};
184
185 use super::*;
186
187 #[fixture]
193 fn log() -> RpcLog {
194 RpcLog {
195 removed: false,
196 log_index: Some("0x0".to_string()),
197 transaction_index: Some("0x0".to_string()),
198 transaction_hash: Some(
199 "0x24058dde7caf5b8b70041de8b27731f20f927365f210247c3e720e947b9098e7".to_string(),
200 ),
201 block_hash: Some(
202 "0xd371b6c7b04ec33d6470f067a82e87d7b294b952bea7a46d7b939b4c7addc275".to_string(),
203 ),
204 block_number: Some("0xb9".to_string()),
205 address: "0x1f98431c8ad98523631ae4a59f267346ea31f984".to_string(),
206 data: "0x000000000000000000000000000000000000000000000000000000000000003c000000000000000000000000b9fc136980d98c034a529aadbd5651c087365d5f".to_string(),
207 topics: vec![
208 "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118".to_string(),
209 "0x0000000000000000000000002e5353426c89f4ecd52d1036da822d47e73376c4".to_string(),
210 "0x000000000000000000000000838930cfe7502dd36b0b1ebbef8001fbf94f3bfb".to_string(),
211 "0x0000000000000000000000000000000000000000000000000000000000000bb8".to_string(),
212 ],
213 }
214 }
215
216 #[rstest]
217 fn test_decode_hex_with_prefix() {
218 let result = decode_hex("0x1234").unwrap();
219 assert_eq!(result, vec![0x12, 0x34]);
220 }
221
222 #[rstest]
223 fn test_decode_hex_without_prefix() {
224 let result = decode_hex("1234").unwrap();
225 assert_eq!(result, vec![0x12, 0x34]);
226 }
227
228 #[rstest]
229 fn test_parse_hex_u64_block_185() {
230 assert_eq!(parse_hex_u64("0xb9").unwrap(), 185);
232 assert_eq!(parse_hex_u64("b9").unwrap(), 185);
233 }
234
235 #[rstest]
236 fn test_parse_hex_u32() {
237 assert_eq!(parse_hex_u32("0x0").unwrap(), 0);
238 assert_eq!(parse_hex_u32("0xbb8").unwrap(), 3000); }
240
241 #[rstest]
242 fn test_extract_block_number(log: RpcLog) {
243 assert_eq!(extract_block_number(&log).unwrap(), 185);
244 }
245
246 #[rstest]
247 fn test_extract_transaction_hash(log: RpcLog) {
248 assert_eq!(
249 extract_transaction_hash(&log).unwrap(),
250 "0x24058dde7caf5b8b70041de8b27731f20f927365f210247c3e720e947b9098e7"
251 );
252 }
253
254 #[rstest]
255 fn test_extract_transaction_index(log: RpcLog) {
256 assert_eq!(extract_transaction_index(&log).unwrap(), 0);
257 }
258
259 #[rstest]
260 fn test_extract_log_index(log: RpcLog) {
261 assert_eq!(extract_log_index(&log).unwrap(), 0);
262 }
263
264 #[rstest]
265 fn test_extract_address(log: RpcLog) {
266 let address = extract_address(&log).unwrap();
267 assert_eq!(
269 address.to_string().to_lowercase(),
270 "0x1f98431c8ad98523631ae4a59f267346ea31f984"
271 );
272 }
273
274 #[rstest]
275 fn test_extract_address_from_topic_token0(log: RpcLog) {
276 let address = extract_address_from_topic(&log, 1, "token0").unwrap();
277 assert_eq!(
278 address.to_string().to_lowercase(),
279 "0x2e5353426c89f4ecd52d1036da822d47e73376c4"
280 );
281 }
282
283 #[rstest]
284 fn test_extract_address_from_topic_token1(log: RpcLog) {
285 let address = extract_address_from_topic(&log, 2, "token1").unwrap();
286 assert_eq!(
287 address.to_string().to_lowercase(),
288 "0x838930cfe7502dd36b0b1ebbef8001fbf94f3bfb"
289 );
290 }
291
292 #[rstest]
293 fn test_extract_data_bytes(log: RpcLog) {
294 let data = extract_data_bytes(&log).unwrap();
295 assert_eq!(data.len(), 64); assert_eq!(data[31], 0x3c);
299 }
300
301 #[rstest]
302 fn test_validate_event_signature_pool_created(log: RpcLog) {
303 let expected = "783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118";
304 assert!(validate_event_signature(&log, expected, "PoolCreated").is_ok());
305 }
306
307 #[rstest]
308 fn test_validate_event_signature_mismatch(log: RpcLog) {
309 let wrong = "c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";
311 let result = validate_event_signature(&log, wrong, "Swap");
312 assert!(result.is_err());
313 assert!(
314 result
315 .unwrap_err()
316 .to_string()
317 .contains("Invalid event signature")
318 );
319 }
320}