diff --git a/src/cli/call.rs b/src/cli/call.rs index e4edc27..a583513 100644 --- a/src/cli/call.rs +++ b/src/cli/call.rs @@ -1,5 +1,5 @@ use super::trace::{self, TraceError, TraceOpts}; -use crate::utils::value_parser; +use crate::utils::{hex_utils, value_parser}; use clap::Parser; use serde_json::json; @@ -143,8 +143,8 @@ pub fn run(args: CallArgs) -> Result<(), TraceError> { fn parse_block_id(block: &str) -> Result { match block { "latest" | "earliest" | "pending" | "safe" | "finalized" => Ok(block.to_string()), - s if s.starts_with("0x") || s.starts_with("0X") => { - let hex = &s[2..]; + s if hex_utils::require_0x(s).is_some() => { + let hex = hex_utils::require_0x(s).unwrap(); if hex.is_empty() || !hex.chars().all(|c| c.is_ascii_hexdigit()) { return Err(TraceError::InvalidInput(format!( "--block: invalid hex block number '{s}'" diff --git a/src/cli/clean.rs b/src/cli/clean.rs index 6bee798..9bdec67 100644 --- a/src/cli/clean.rs +++ b/src/cli/clean.rs @@ -1,6 +1,7 @@ use crate::utils::disk_cache::{CacheError, DiskCache}; use clap::Parser; use std::fs; +use std::path::Path; use thiserror::Error; const CACHE_KINDS: &[&str] = &["selectors", "contracts"]; @@ -48,7 +49,7 @@ pub fn run(args: CleanArgs) -> Result<(), CleanError> { } if !found_any { - match DiskCache::cache_path(CACHE_KINDS[0]).and_then(|p| p.parent().map(|d| d.to_owned())) { + match DiskCache::cache_path(CACHE_KINDS[0]).and_then(|p| p.parent().map(Path::to_owned)) { Some(dir) => println!("no cache files found in {}", dir.display()), None => println!("no cache files found (could not determine cache directory)"), } diff --git a/src/cli/trace.rs b/src/cli/trace.rs index f49195a..f479b20 100644 --- a/src/cli/trace.rs +++ b/src/cli/trace.rs @@ -144,8 +144,7 @@ fn resolve_rpc_url(url_or_alias: Option) -> Result { /// Validate that a string is a 0x-prefixed Ethereum address (40 hex chars). pub fn validate_address(addr: &str, field: &str) -> Result<(), TraceError> { - let hex = addr - .strip_prefix("0x") + let hex = hex_utils::require_0x(addr) .ok_or_else(|| TraceError::InvalidInput(format!("{field}: missing 0x prefix")))?; if hex.len() != 40 { return Err(TraceError::InvalidInput(format!( @@ -163,8 +162,7 @@ pub fn validate_address(addr: &str, field: &str) -> Result<(), TraceError> { /// Validate that a string is 0x-prefixed hex data with an even number of hex chars. pub fn validate_hex(data: &str, field: &str) -> Result<(), TraceError> { - let hex = data - .strip_prefix("0x") + let hex = hex_utils::require_0x(data) .ok_or_else(|| TraceError::InvalidInput(format!("{field}: missing 0x prefix")))?; if !hex.chars().all(|c| c.is_ascii_hexdigit()) { return Err(TraceError::InvalidInput(format!( @@ -206,9 +204,7 @@ fn fetch_chain_id(client: &Client, rpc_url: &str) -> Result /// Parse a hex chain ID (e.g. `"0x1"`, `"0xa"`) into a decimal string. fn parse_chain_id_hex(hex_str: &str) -> Result { - let stripped = hex_str - .strip_prefix("0x") - .or_else(|| hex_str.strip_prefix("0X")) + let stripped = hex_utils::require_0x(hex_str) .ok_or_else(|| TraceError::Decode(format!("eth_chainId: invalid hex '{hex_str}'")))?; u64::from_str_radix(stripped, 16) @@ -477,7 +473,7 @@ fn print_call( } } else if decode_attempted { if let Some(input) = &node.input { - let s = input.strip_prefix("0x").unwrap_or(input); + let s = hex_utils::strip_0x(input); if s.len() > 8 { println!( "{meta_prefix}{}", @@ -547,7 +543,7 @@ fn format_call_desc( } fn extract_selector(input: &str) -> Option { - let s = input.strip_prefix("0x").unwrap_or(input); + let s = hex_utils::strip_0x(input); s.get(..8).map(|sel| format!("0x{sel}")) } @@ -627,6 +623,16 @@ mod tests { assert!(validate_hex("0xab", "data").is_ok()); } + #[test] + fn test_validate_address_uppercase_prefix() { + assert!(validate_address("0XdAC17F958D2ee523a2206206994597C13D831ec7", "to").is_ok()); + } + + #[test] + fn test_validate_hex_uppercase_prefix() { + assert!(validate_hex("0Xa9059cbb", "data").is_ok()); + } + #[test] fn test_parse_chain_id_hex() { assert_eq!(parse_chain_id_hex("0x1").unwrap(), "1"); diff --git a/src/utils/abi_decoder.rs b/src/utils/abi_decoder.rs index 6570d63..386cb4c 100644 --- a/src/utils/abi_decoder.rs +++ b/src/utils/abi_decoder.rs @@ -1,3 +1,4 @@ +use super::hex_utils; use alloy_dyn_abi::{DynSolType, DynSolValue, JsonAbiExt}; use alloy_json_abi::Function; @@ -31,7 +32,7 @@ pub fn can_decode(signature: &str, calldata: &str) -> bool { /// Recognises `Error(string)` (0x08c379a0) and `Panic(uint256)` (0x4e487b71). /// Returns `None` for unknown selectors — use [`decode_custom_revert`] as fallback. pub fn decode_revert_reason(output: &str) -> Option { - let hex = output.strip_prefix("0x").unwrap_or(output); + let hex = hex_utils::strip_0x(output); if hex.len() < 8 { return None; } @@ -66,7 +67,7 @@ pub fn decode_custom_revert( output: &str, resolver: &mut super::selector_resolver::SelectorResolver, ) -> Option { - let hex = output.strip_prefix("0x").unwrap_or(output); + let hex = hex_utils::strip_0x(output); if hex.len() < 8 { return None; } @@ -110,7 +111,7 @@ pub fn format_value(value: &DynSolValue) -> String { } fn decode_hex(input: &str) -> Option> { - hex::decode(input.strip_prefix("0x").unwrap_or(input)).ok() + hex::decode(hex_utils::strip_0x(input)).ok() } fn decode_with_function(signature: &str, data: &[u8]) -> Option> { diff --git a/src/utils/event_formatter.rs b/src/utils/event_formatter.rs index 5190f8a..b3c6a37 100644 --- a/src/utils/event_formatter.rs +++ b/src/utils/event_formatter.rs @@ -72,7 +72,7 @@ fn format_event_raw(topic0: &str, log: &Log) -> String { } if let Some(data) = &log.data { - let stripped = data.strip_prefix("0x").unwrap_or(data); + let stripped = hex_utils::strip_0x(data); if !stripped.is_empty() { parts.push(format!("data: {data}")); } @@ -107,7 +107,7 @@ fn decode_event_params(signature: &str, log: &Log) -> Vec { } fn decode_event_data(signature: &str, data: &str, indexed_count: usize) -> Option> { - let stripped = data.strip_prefix("0x").unwrap_or(data); + let stripped = hex_utils::strip_0x(data); if stripped.is_empty() { return Some(Vec::new()); } @@ -183,7 +183,7 @@ fn format_decoded_values(value: &alloy_dyn_abi::DynSolValue) -> Vec { /// Tries to parse as number (must fit in u128 as a heuristic to distinguish /// numbers from hashes), then detects addresses; otherwise shows full hex. fn format_topic_value(topic: &str) -> String { - let stripped = topic.strip_prefix("0x").unwrap_or(topic); + let stripped = hex_utils::strip_0x(topic); // Heuristic: if the value fits in u128, it's likely a number. // Full 32-byte values (hashes, large IDs) won't fit. diff --git a/src/utils/hex_utils.rs b/src/utils/hex_utils.rs index aaf69ce..6d222ce 100644 --- a/src/utils/hex_utils.rs +++ b/src/utils/hex_utils.rs @@ -1,8 +1,17 @@ use alloy_primitives::U256; -/// Parse a hex string (with or without 0x prefix) as U256. +pub fn strip_0x(s: &str) -> &str { + s.strip_prefix("0x") + .or_else(|| s.strip_prefix("0X")) + .unwrap_or(s) +} + +pub fn require_0x(s: &str) -> Option<&str> { + s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) +} + pub fn parse_hex_u256(s: &str) -> Option { - let s = s.strip_prefix("0x").unwrap_or(s); + let s = strip_0x(s); U256::from_str_radix(s, 16).ok() } @@ -10,6 +19,23 @@ pub fn parse_hex_u256(s: &str) -> Option { mod tests { use super::*; + #[test] + fn test_strip_0x() { + assert_eq!(strip_0x("0xabc"), "abc"); + assert_eq!(strip_0x("0Xabc"), "abc"); + assert_eq!(strip_0x("abc"), "abc"); + assert_eq!(strip_0x("0x"), ""); + assert_eq!(strip_0x("0X"), ""); + } + + #[test] + fn test_require_0x() { + assert_eq!(require_0x("0xabc"), Some("abc")); + assert_eq!(require_0x("0Xabc"), Some("abc")); + assert_eq!(require_0x("abc"), None); + assert_eq!(require_0x("0x"), Some("")); + } + #[test] fn test_parse_hex_u256() { assert_eq!(parse_hex_u256("0x10"), Some(U256::from(16))); diff --git a/src/utils/precompiles.rs b/src/utils/precompiles.rs index 4f3f818..6162285 100644 --- a/src/utils/precompiles.rs +++ b/src/utils/precompiles.rs @@ -3,7 +3,7 @@ /// Precompiled contracts are at addresses 0x01-0x0a (and potentially higher in newer forks). /// Returns `Some((name, signature))` if the address is a known precompile, `None` otherwise. pub fn get_precompile_info(address: &str) -> Option<(&'static str, &'static str)> { - let addr = address.strip_prefix("0x").unwrap_or(address); + let addr = super::hex_utils::strip_0x(address); // Full 40-char addresses: precompiles have 38+ leading zeros. // Short-circuit: if the non-zero portion is beyond 0x0a, it's not a precompile. diff --git a/src/utils/value_parser.rs b/src/utils/value_parser.rs index 03e1977..cebf329 100644 --- a/src/utils/value_parser.rs +++ b/src/utils/value_parser.rs @@ -1,3 +1,4 @@ +use super::hex_utils; use alloy_primitives::U256; /// Parse a value string into a `0x`-prefixed hex wei amount. @@ -9,7 +10,7 @@ use alloy_primitives::U256; pub fn parse_value(s: &str) -> Result { let s = s.trim(); - if let Some(hex) = s.strip_prefix("0x") { + if let Some(hex) = hex_utils::require_0x(s) { let v = U256::from_str_radix(hex, 16).map_err(|_| format!("invalid hex value: {s}"))?; return Ok(format!("0x{v:x}")); } @@ -95,6 +96,14 @@ mod tests { assert_eq!(parse_value("1000000000wei").unwrap(), "0x3b9aca00"); } + #[test] + fn test_hex_uppercase_prefix() { + assert_eq!( + parse_value("0Xde0b6b3a7640000").unwrap(), + "0xde0b6b3a7640000" + ); + } + #[test] fn test_invalid() { assert!(parse_value("abc").is_err());