Skip to content

Commit 1c1e653

Browse files
authored
feat: inspect transaction inspection module (#221)
* feat(inspect): init inspect * feat(inspect): init transaction tracing (and decoding) * feat(inspect): tracing, `Parameterize` trait * feat(inspect): implement `DecodedTransactionTrace` * feat(inspect): log decoding, joining to trace at correct addresses * feat(inspect): impl `TryFrom<Log> for DecodedLog` * feat(resources): add trace display * feat(inspect): finalize trace builder with aliases * fix(doctests): typo * feat(inspect): storage diff in trace * fix(hex): `U256::to_lower_hex()` padding fix * chore(inspect): add inspect example * chore(inspect): add tests * chore(inspect): add ex to workspace * chore: fix typo
1 parent c135098 commit 1c1e653

31 files changed

Lines changed: 1841 additions & 157 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cache/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"]
1111
clap = { version = "3.1.18", features = ["derive"] }
1212
serde = { version = "1.0", features = ["derive"] }
1313
bincode = "1.3.3"
14+
serde_json = "1.0.108"

cache/src/lib.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,9 @@ where
194194
None => return None,
195195
};
196196

197-
let binary_vec = decode_hex(&binary_string);
197+
let binary_vec = decode_hex(&binary_string).ok()?;
198198

199-
if binary_vec.is_err() {
200-
return None;
201-
}
202-
203-
let cache: Cache<T> = match bincode::deserialize::<Cache<T>>(&binary_vec.unwrap()) {
199+
let cache: Cache<T> = match bincode::deserialize::<Cache<T>>(&binary_vec) {
204200
Ok(c) => {
205201
// check if the cache has expired, if so, delete it and return None
206202
if c.expiry <
@@ -233,7 +229,11 @@ where
233229
/// store_cache("store_cache_key2", "value", Some(60 * 60 * 24));
234230
/// ```
235231
#[allow(deprecated)]
236-
pub fn store_cache<T>(key: &str, value: T, expiry: Option<u64>)
232+
pub fn store_cache<T>(
233+
key: &str,
234+
value: T,
235+
expiry: Option<u64>,
236+
) -> Result<(), Box<dyn std::error::Error>>
237237
where
238238
T: Serialize, {
239239
let home = home_dir().unwrap();
@@ -247,9 +247,12 @@ where
247247
);
248248

249249
let cache = Cache { value, expiry };
250-
let encoded: Vec<u8> = bincode::serialize(&cache).unwrap();
250+
let encoded: Vec<u8> = bincode::serialize(&cache)
251+
.map_err(|e| format!("Failed to serialize cache object: {:?}", e))?;
251252
let binary_string = encode_hex(encoded);
252253
write_file(cache_file.to_str().unwrap(), &binary_string);
254+
255+
Ok(())
253256
}
254257

255258
/// Cache subcommand handler
@@ -289,6 +292,7 @@ pub fn cache(args: CacheArgs) -> Result<(), Box<dyn std::error::Error>> {
289292
}
290293

291294
#[allow(deprecated)]
295+
#[allow(unused_must_use)]
292296
#[cfg(test)]
293297
mod tests {
294298
use crate::{delete_cache, exists, keys, read_cache, store_cache};

cli/src/main.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use heimdall_core::{
2727
decompile::{decompile, out::abi::ABIStructure, DecompilerArgs},
2828
disassemble::{disassemble, DisassemblerArgs},
2929
dump::{dump, DumpArgs},
30+
inspect::{inspect, InspectArgs},
3031
snapshot::{snapshot, util::csv::generate_csv, SnapshotArgs},
3132
};
3233
use tui::{backend::CrosstermBackend, Terminal};
@@ -65,9 +66,16 @@ pub enum Subcommands {
6566

6667
#[clap(name = "dump", about = "Dump the value of all storage slots accessed by a contract")]
6768
Dump(DumpArgs),
69+
70+
#[clap(
71+
name = "inspect",
72+
about = "Detailed inspection of Ethereum transactions, including calldata & trace decoding, log visualization, and more"
73+
)]
74+
Inspect(InspectArgs),
75+
6876
#[clap(
6977
name = "snapshot",
70-
about = "Infer functiogn information from bytecode, including access control, gas
78+
about = "Infer function information from bytecode, including access control, gas
7179
consumption, storage accesses, event emissions, and more"
7280
)]
7381
Snapshot(SnapshotArgs),
@@ -225,7 +233,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
225233
cmd.openai_api_key = configuration.openai_api_key;
226234
}
227235

228-
// set cmd.verbose to 6
236+
// set cmd.verbose to 5
229237
cmd.verbose = clap_verbosity_flag::Verbosity::new(5, 0);
230238

231239
let _ = decode(cmd).await;
@@ -330,6 +338,50 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
330338
}
331339
}
332340

341+
Subcommands::Inspect(mut cmd) => {
342+
// if the user has not specified a rpc url, use the default
343+
if cmd.rpc_url.as_str() == "" {
344+
cmd.rpc_url = configuration.rpc_url;
345+
}
346+
347+
// if the user has not specified a transpose api key, use the default
348+
if cmd.transpose_api_key.is_none() {
349+
cmd.transpose_api_key = Some(configuration.transpose_api_key);
350+
}
351+
352+
// if the user has passed an output filename, override the default filename
353+
let mut filename = "decoded_trace.json".to_string();
354+
let given_name = cmd.name.as_str();
355+
356+
if !given_name.is_empty() {
357+
filename = format!("{}-{}", given_name, filename);
358+
}
359+
360+
// set cmd.verbose to 5
361+
cmd.verbose = clap_verbosity_flag::Verbosity::new(5, 0);
362+
363+
let inspect_result = inspect(cmd.clone()).await?;
364+
365+
if cmd.output == "print" {
366+
let mut output_str = String::new();
367+
368+
if let Some(decoded_trace) = inspect_result.decoded_trace {
369+
output_str.push_str(&format!(
370+
"Decoded Trace:\n\n{}\n",
371+
serde_json::to_string_pretty(&decoded_trace).unwrap()
372+
));
373+
}
374+
375+
print_with_less(&output_str).await?;
376+
} else if let Some(decoded_trace) = inspect_result.decoded_trace {
377+
// write decoded trace with serde
378+
let output_path =
379+
build_output_path(&cmd.output, &cmd.target, &cmd.rpc_url, &filename).await?;
380+
381+
write_file(&output_path, &serde_json::to_string_pretty(&decoded_trace).unwrap());
382+
}
383+
}
384+
333385
Subcommands::Config(cmd) => {
334386
config(cmd);
335387
}

cli/src/output.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::{env, io::Write};
22

3-
use heimdall_common::{constants::ADDRESS_REGEX, ether::rpc};
3+
use heimdall_common::{
4+
constants::{ADDRESS_REGEX, TRANSACTION_HASH_REGEX},
5+
ether::rpc,
6+
};
47

58
/// build a standardized output path for the given parameters. follows the following cases:
69
/// - if `output` is `print`, return `None`
@@ -19,7 +22,7 @@ pub async fn build_output_path(
1922
// get the current working directory
2023
let cwd = env::current_dir()?.into_os_string().into_string().unwrap();
2124

22-
if ADDRESS_REGEX.is_match(target)? {
25+
if ADDRESS_REGEX.is_match(target)? || TRANSACTION_HASH_REGEX.is_match(target)? {
2326
let chain_id = rpc::chain_id(rpc_url).await?;
2427
return Ok(format!("{}/output/{}/{}/{}", cwd, chain_id, target, filename));
2528
} else {

0 commit comments

Comments
 (0)