Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/cli/call.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -143,8 +143,8 @@ pub fn run(args: CallArgs) -> Result<(), TraceError> {
fn parse_block_id(block: &str) -> Result<String, TraceError> {
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}'"
Expand Down
3 changes: 2 additions & 1 deletion src/cli/clean.rs
Original file line number Diff line number Diff line change
@@ -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"];
Expand Down Expand Up @@ -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)"),
}
Expand Down
24 changes: 15 additions & 9 deletions src/cli/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ fn resolve_rpc_url(url_or_alias: Option<String>) -> Result<String, TraceError> {

/// 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!(
Expand All @@ -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!(
Expand Down Expand Up @@ -206,9 +204,7 @@ fn fetch_chain_id(client: &Client, rpc_url: &str) -> Result<String, TraceError>

/// Parse a hex chain ID (e.g. `"0x1"`, `"0xa"`) into a decimal string.
fn parse_chain_id_hex(hex_str: &str) -> Result<String, TraceError> {
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)
Expand Down Expand Up @@ -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}{}",
Expand Down Expand Up @@ -547,7 +543,7 @@ fn format_call_desc(
}

fn extract_selector(input: &str) -> Option<String> {
let s = input.strip_prefix("0x").unwrap_or(input);
let s = hex_utils::strip_0x(input);
s.get(..8).map(|sel| format!("0x{sel}"))
}

Expand Down Expand Up @@ -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");
Expand Down
7 changes: 4 additions & 3 deletions src/utils/abi_decoder.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::hex_utils;
use alloy_dyn_abi::{DynSolType, DynSolValue, JsonAbiExt};
use alloy_json_abi::Function;

Expand Down Expand Up @@ -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<String> {
let hex = output.strip_prefix("0x").unwrap_or(output);
let hex = hex_utils::strip_0x(output);
if hex.len() < 8 {
return None;
}
Expand Down Expand Up @@ -66,7 +67,7 @@ pub fn decode_custom_revert(
output: &str,
resolver: &mut super::selector_resolver::SelectorResolver,
) -> Option<String> {
let hex = output.strip_prefix("0x").unwrap_or(output);
let hex = hex_utils::strip_0x(output);
if hex.len() < 8 {
return None;
}
Expand Down Expand Up @@ -110,7 +111,7 @@ pub fn format_value(value: &DynSolValue) -> String {
}

fn decode_hex(input: &str) -> Option<Vec<u8>> {
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<Vec<(DynSolType, DynSolValue)>> {
Expand Down
6 changes: 3 additions & 3 deletions src/utils/event_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"));
}
Expand Down Expand Up @@ -107,7 +107,7 @@ fn decode_event_params(signature: &str, log: &Log) -> Vec<String> {
}

fn decode_event_data(signature: &str, data: &str, indexed_count: usize) -> Option<Vec<String>> {
let stripped = data.strip_prefix("0x").unwrap_or(data);
let stripped = hex_utils::strip_0x(data);
if stripped.is_empty() {
return Some(Vec::new());
}
Expand Down Expand Up @@ -183,7 +183,7 @@ fn format_decoded_values(value: &alloy_dyn_abi::DynSolValue) -> Vec<String> {
/// 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.
Expand Down
30 changes: 28 additions & 2 deletions src/utils/hex_utils.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
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<U256> {
let s = s.strip_prefix("0x").unwrap_or(s);
let s = strip_0x(s);
U256::from_str_radix(s, 16).ok()
}

#[cfg(test)]
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)));
Expand Down
2 changes: 1 addition & 1 deletion src/utils/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion src/utils/value_parser.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::hex_utils;
use alloy_primitives::U256;

/// Parse a value string into a `0x`-prefixed hex wei amount.
Expand All @@ -9,7 +10,7 @@ use alloy_primitives::U256;
pub fn parse_value(s: &str) -> Result<String, String> {
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}"));
}
Expand Down Expand Up @@ -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());
Expand Down