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