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
9 changes: 4 additions & 5 deletions src/cli/clean.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use crate::utils::disk_cache::{CacheError, DiskCache};
use crate::utils::disk_cache::{CacheError, DiskCache, ALL_CACHE_KINDS};
use clap::Parser;
use std::fs;
use std::path::Path;
use thiserror::Error;

const CACHE_KINDS: &[&str] = &["selectors", "contracts"];

#[derive(Parser, Debug)]
pub struct CleanArgs {
/// Only remove unknown (unresolved) entries from the cache.
Expand All @@ -26,7 +24,7 @@ pub enum CleanError {
pub fn run(args: CleanArgs) -> Result<(), CleanError> {
let mut found_any = false;

for kind in CACHE_KINDS {
for kind in ALL_CACHE_KINDS {
let Some(path) = DiskCache::cache_path(kind) else {
continue;
};
Expand All @@ -49,7 +47,8 @@ pub fn run(args: CleanArgs) -> Result<(), CleanError> {
}

if !found_any {
match DiskCache::cache_path(CACHE_KINDS[0]).and_then(|p| p.parent().map(Path::to_owned)) {
match DiskCache::cache_path(ALL_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
71 changes: 29 additions & 42 deletions src/cli/trace.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use crate::utils::{
color::Palette,
contract_resolver::ContractResolver,
event_formatter::Log,
hex_utils, rpc_url,
selector_resolver::SelectorResolver,
storage_diff::{self, PrestateDiff},
trace_renderer,
trace_renderer::{self, CallTrace},
};
use clap::Parser;
use reqwest::blocking::Client;
Expand Down Expand Up @@ -111,24 +110,6 @@ struct RpcError {
message: String,
}

/// Result shape for geth-style `callTracer`.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallTrace {
#[serde(rename = "type")]
pub call_type: Option<String>,
pub to: Option<String>,
pub value: Option<String>,
pub gas_used: Option<String>,
pub input: Option<String>,
pub output: Option<String>,
pub error: Option<String>,
#[serde(default)]
pub logs: Vec<Log>,
#[serde(default)]
pub calls: Vec<CallTrace>,
}

/// Create a pre-configured HTTP client for RPC calls.
fn create_client(no_proxy: bool) -> Result<Client, TraceError> {
let mut builder = Client::builder().timeout(Duration::from_secs(60));
Expand All @@ -150,17 +131,19 @@ 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 = hex_utils::require_0x(addr)
.ok_or_else(|| TraceError::InvalidInput(format!("{field}: missing 0x prefix")))?;
if hex.len() != 40 {
if !hex_utils::is_valid_address(addr) {
return Err(TraceError::InvalidInput(format!(
"{field}: expected 40 hex chars, got {}",
hex.len()
"{field}: expected 0x-prefixed 40-char hex address"
)));
}
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(())
}

/// Validate that a string is a 0x-prefixed transaction hash (64 hex chars).
pub fn validate_tx_hash(hash: &str, field: &str) -> Result<(), TraceError> {
if !hex_utils::is_valid_tx_hash(hash) {
return Err(TraceError::InvalidInput(format!(
"{field}: invalid hex characters"
"{field}: expected 0x-prefixed 64-char hex hash"
)));
}
Ok(())
Expand Down Expand Up @@ -216,20 +199,8 @@ pub fn prestate_tracer_config() -> serde_json::Value {
/// Fetch the chain ID from the RPC node as a decimal string (e.g. `"1"`).
fn fetch_chain_id(client: &Client, rpc_url: &str) -> Result<String, TraceError> {
let payload = rpc_payload(1, "eth_chainId", serde_json::json!([]));

let resp = client.post(rpc_url).json(&payload).send()?;
let body: RpcResponse<String> = resp
.json()
.map_err(|e| TraceError::Decode(format!("eth_chainId: invalid JSON: {e}")))?;

if let Some(err) = body.error {
return Err(TraceError::Rpc(err.message, err.code));
}

let hex_str = body
.result
.ok_or_else(|| TraceError::Decode("eth_chainId: missing result".into()))?;

let hex_str: String = parse_rpc_response(resp)?;
parse_chain_id_hex(&hex_str)
}

Expand Down Expand Up @@ -298,8 +269,9 @@ pub fn execute_and_print(
Palette::auto()
};

let mut selector_resolver = SelectorResolver::new(client.clone(), opts.resolve_selectors);
let mut contract_resolver = ContractResolver::new(client, chain_id, opts.resolve_contracts);
let mut selector_resolver = SelectorResolver::new(client.clone(), opts.resolve_selectors, None);
let mut contract_resolver =
ContractResolver::new(client, chain_id, opts.resolve_contracts, None);
trace_renderer::print_trace(
&call_trace,
&mut selector_resolver,
Expand Down Expand Up @@ -381,6 +353,21 @@ mod tests {
assert!(validate_hex("0xab", "data").is_ok());
}

#[test]
fn test_validate_tx_hash() {
assert!(validate_tx_hash(
"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"hash"
)
.is_ok());
assert!(validate_tx_hash("0x1234", "hash").is_err());
assert!(validate_tx_hash(
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"hash"
)
.is_err());
}

#[test]
fn test_validate_address_uppercase_prefix() {
assert!(validate_address("0XdAC17F958D2ee523a2206206994597C13D831ec7", "to").is_ok());
Expand Down
8 changes: 1 addition & 7 deletions src/cli/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ pub struct TxArgs {
}

pub fn run(args: TxArgs) -> Result<(), TraceError> {
trace::validate_hex(&args.tx_hash, "tx_hash")?;
if args.tx_hash.len() != 66 {
return Err(TraceError::InvalidInput(format!(
"tx_hash: expected 64 hex chars, got {}",
args.tx_hash.len() - 2
)));
}
trace::validate_tx_hash(&args.tx_hash, "tx_hash")?;

let TxArgs { tx_hash, opts } = args;

Expand Down
2 changes: 2 additions & 0 deletions src/utils/abi_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ mod tests {
let mut resolver = crate::utils::selector_resolver::SelectorResolver::new(
reqwest::blocking::Client::new(),
false,
None,
);
let output = "0x08c379a0\
0000000000000000000000000000000000000000000000000000000000000020\
Expand All @@ -279,6 +280,7 @@ mod tests {
let mut resolver = crate::utils::selector_resolver::SelectorResolver::new(
reqwest::blocking::Client::new(),
false,
None,
);
let output = "0xdeadbeef0000000000000000000000000000000000000000000000000000000000000001";
assert!(decode_custom_revert(output, &mut resolver).is_none());
Expand Down
57 changes: 40 additions & 17 deletions src/utils/contract_resolver.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
use super::disk_cache::{CacheLookup, DiskCache};
use super::disk_cache::{CacheLookup, DiskCache, CONTRACT_CACHE};
use super::hex_utils;
use reqwest::blocking::Client;
use serde::Deserialize;

const DEFAULT_SOURCIFY_SERVER_URL: &str = "https://sourcify.dev/server/";

#[derive(Deserialize)]
struct ContractInfo {
compilation: Option<Compilation>,
}

#[derive(Deserialize)]
struct Compilation {
name: Option<String>,
}

/// Best-effort contract name resolver using Sourcify's v2 API with disk caching.
pub struct ContractResolver {
client: Client,
Expand All @@ -14,15 +26,21 @@ pub struct ContractResolver {
}

impl ContractResolver {
pub fn new(client: Client, chain_id: Option<String>, enabled: bool) -> Self {
let mut base_url = std::env::var("SOURCIFY_SERVER_URL")
.unwrap_or_else(|_| DEFAULT_SOURCIFY_SERVER_URL.to_owned());
pub fn new(
client: Client,
chain_id: Option<String>,
enabled: bool,
base_url: Option<String>,
) -> Self {
let mut base_url = base_url
.or_else(|| std::env::var("SOURCIFY_SERVER_URL").ok())
.unwrap_or_else(|| DEFAULT_SOURCIFY_SERVER_URL.to_owned());
if !base_url.ends_with('/') {
base_url.push('/');
}
Self {
client,
disk_cache: DiskCache::load("contracts"),
disk_cache: DiskCache::load(CONTRACT_CACHE),
base_url,
chain_id,
enabled,
Expand All @@ -40,7 +58,7 @@ impl ContractResolver {
return None;
}
let chain_id = self.chain_id.as_deref()?;
if !address.starts_with("0x") || address.len() != 42 {
if !hex_utils::is_valid_address(address) {
return None;
}

Expand Down Expand Up @@ -75,21 +93,26 @@ impl ContractResolver {
}
};

let Some(body) = resp.json::<serde_json::Value>().ok() else {
let Ok(info) = resp.json::<ContractInfo>() else {
if self.warning.is_none() {
self.warning = Some(format!(
"sourcify contract response parse failed for {address}, results may be incomplete"
));
}
self.disk_cache.insert_transient_miss(cache_key);
return None;
};

match body["compilation"]["name"].as_str() {
Some(name) if !name.is_empty() => {
let name = name.to_owned();
self.disk_cache.insert(cache_key, name.clone());
Some(name)
}
_ => {
self.disk_cache.insert_miss(cache_key);
None
}
if let Some(name) = info
.compilation
.and_then(|c| c.name)
.filter(|n| !n.is_empty())
{
self.disk_cache.insert(cache_key, name.clone());
Some(name)
} else {
self.disk_cache.insert_miss(cache_key);
None
}
}
}
4 changes: 4 additions & 0 deletions src/utils/disk_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use thiserror::Error;

const CACHE_MISS_MARKER: &str = "<UNKNOWN>";

pub const SELECTOR_CACHE: &str = "selectors";
pub const CONTRACT_CACHE: &str = "contracts";
pub const ALL_CACHE_KINDS: &[&str] = &[SELECTOR_CACHE, CONTRACT_CACHE];

/// Result of looking up a key in the cache.
#[derive(Debug)]
pub enum CacheLookup<'a> {
Expand Down
36 changes: 36 additions & 0 deletions src/utils/hex_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ pub fn require_0x(s: &str) -> Option<&str> {
s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
}

pub fn is_valid_address(s: &str) -> bool {
require_0x(s).is_some_and(|h| h.len() == 40 && h.chars().all(|c| c.is_ascii_hexdigit()))
}

pub fn is_valid_tx_hash(s: &str) -> bool {
require_0x(s).is_some_and(|h| h.len() == 64 && h.chars().all(|c| c.is_ascii_hexdigit()))
}

pub fn parse_hex_u256(s: &str) -> Option<U256> {
let s = strip_0x(s);
U256::from_str_radix(s, 16).ok()
Expand All @@ -36,6 +44,34 @@ mod tests {
assert_eq!(require_0x("0x"), Some(""));
}

#[test]
fn test_is_valid_address() {
assert!(is_valid_address(
"0xdAC17F958D2ee523a2206206994597C13D831ec7"
));
assert!(is_valid_address(
"0XdAC17F958D2ee523a2206206994597C13D831ec7"
));
assert!(!is_valid_address(
"dAC17F958D2ee523a2206206994597C13D831ec7"
));
assert!(!is_valid_address("0x1234"));
assert!(!is_valid_address(
"0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ"
));
}

#[test]
fn test_is_valid_tx_hash() {
assert!(is_valid_tx_hash(
"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
));
assert!(!is_valid_tx_hash("0x1234"));
assert!(!is_valid_tx_hash(
"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
));
}

#[test]
fn test_parse_hex_u256() {
assert_eq!(parse_hex_u256("0x10"), Some(U256::from(16)));
Expand Down
Loading