From b78691f231488f343fff686123eb4047c885bed1 Mon Sep 17 00:00:00 2001 From: Jonathan Becker Date: Sat, 30 Sep 2023 12:04:50 -0400 Subject: [PATCH] :sparkles: feat: add lib examples (#155) * :sparkles: feat: init examples * :sparkles: feat: more examples, fix some async things * :wrench: fix: remove unused `err` var --- Cargo.lock | 92 ++++++++++++++---- Cargo.toml | 12 ++- cli/src/main.rs | 70 +------------- common/Cargo.toml | 2 + common/src/ether/selectors.rs | 4 +- common/src/ether/signatures.rs | 28 ++++-- common/src/ether/tests.rs | 61 ++++++------ common/src/resources/transpose.rs | 31 +++--- common/src/utils/http.rs | 50 +++++----- common/src/utils/tests.rs | 6 +- common/src/utils/version.rs | 4 +- core/Cargo.toml | 1 + core/src/cfg/mod.rs | 27 +++++- core/src/decode/mod.rs | 152 +++++++----------------------- core/src/decompile/mod.rs | 34 ++++++- core/src/dump/mod.rs | 36 ++++++- core/src/dump/util/csv.rs | 1 + core/src/snapshot/mod.rs | 28 +++++- examples/cfg/Cargo.toml | 12 +++ examples/cfg/src/main.rs | 16 ++++ examples/decode/Cargo.toml | 12 +++ examples/decode/src/main.rs | 14 +++ examples/decompile/Cargo.toml | 12 +++ examples/decompile/src/main.rs | 16 ++++ examples/dump/Cargo.toml | 12 +++ examples/dump/src/main.rs | 19 ++++ examples/snapshot/Cargo.toml | 12 +++ examples/snapshot/src/main.rs | 16 ++++ 28 files changed, 478 insertions(+), 302 deletions(-) create mode 100644 examples/cfg/Cargo.toml create mode 100644 examples/cfg/src/main.rs create mode 100644 examples/decode/Cargo.toml create mode 100644 examples/decode/src/main.rs create mode 100644 examples/decompile/Cargo.toml create mode 100644 examples/decompile/src/main.rs create mode 100644 examples/dump/Cargo.toml create mode 100644 examples/dump/src/main.rs create mode 100644 examples/snapshot/Cargo.toml create mode 100644 examples/snapshot/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 7f78e2cf..c6fc69eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -98,7 +109,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -1087,7 +1098,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "syn 2.0.18", + "syn 2.0.37", "toml", "walkdir", ] @@ -1105,7 +1116,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -1131,7 +1142,7 @@ dependencies = [ "serde", "serde_json", "strum", - "syn 2.0.18", + "syn 2.0.37", "tempfile", "thiserror", "tiny-keccak", @@ -1456,7 +1467,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -1646,6 +1657,8 @@ name = "heimdall-common" version = "0.6.0" dependencies = [ "async-openai", + "async-recursion", + "async-trait", "clap", "clap-verbosity-flag", "colored", @@ -1683,6 +1696,7 @@ dependencies = [ "clap-verbosity-flag", "colored", "crossterm 0.26.1", + "derive_builder", "ethers", "fancy-regex", "heimdall-cache", @@ -1698,6 +1712,46 @@ dependencies = [ "tui", ] +[[package]] +name = "heimdall-rs-cfg-example" +version = "0.1.0" +dependencies = [ + "heimdall-core", + "tokio", +] + +[[package]] +name = "heimdall-rs-decode-example" +version = "0.1.0" +dependencies = [ + "heimdall-core", + "tokio", +] + +[[package]] +name = "heimdall-rs-decompile-example" +version = "0.1.0" +dependencies = [ + "heimdall-core", + "tokio", +] + +[[package]] +name = "heimdall-rs-dump-example" +version = "0.1.0" +dependencies = [ + "heimdall-core", + "tokio", +] + +[[package]] +name = "heimdall-rs-snapshot-example" +version = "0.1.0" +dependencies = [ + "heimdall-core", + "tokio", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -2235,7 +2289,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2313,7 +2367,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2522,7 +2576,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2578,7 +2632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" dependencies = [ "proc-macro2", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -2631,9 +2685,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -3125,7 +3179,7 @@ checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -3392,9 +3446,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -3464,7 +3518,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -3534,7 +3588,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -3657,7 +3711,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", ] [[package]] @@ -3877,7 +3931,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -3911,7 +3965,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index f7f8b76d..9ebc7741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,13 @@ [workspace] -members = ["common", "config", "core", "cli"] +members = [ + "common", + "config", + "core", + "cli", + "examples/cfg", + "examples/dump", + "examples/decode", + "examples/snapshot", + "examples/decompile" +] version = "0.6.0" diff --git a/cli/src/main.rs b/cli/src/main.rs index 5583757c..52472989 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -109,17 +109,6 @@ async fn main() -> Result<(), Box> { cmd.rpc_url = configuration.rpc_url; } - // set logger environment variable if not already set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var( - "RUST_LOG", - match cmd.verbose.log_level() { - Some(level) => level.as_str(), - None => "SILENT", - }, - ); - } - let assembly = disassemble(cmd.clone()).await?; // write to file @@ -137,17 +126,6 @@ async fn main() -> Result<(), Box> { cmd.rpc_url = configuration.rpc_url; } - // set logger environment variable if not already set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var( - "RUST_LOG", - match cmd.verbose.log_level() { - Some(level) => level.as_str(), - None => "SILENT", - }, - ); - } - let result = decompile(cmd.clone()).await?; // write to file @@ -209,18 +187,7 @@ async fn main() -> Result<(), Box> { cmd.openai_api_key = configuration.openai_api_key; } - // set logger environment variable if not already set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var( - "RUST_LOG", - match cmd.verbose.log_level() { - Some(level) => level.as_str(), - None => "SILENT", - }, - ); - } - - decode(cmd).await; + let _ = decode(cmd).await; } Subcommands::CFG(mut cmd) => { @@ -229,17 +196,6 @@ async fn main() -> Result<(), Box> { cmd.rpc_url = configuration.rpc_url; } - // set logger environment variable if not already set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var( - "RUST_LOG", - match cmd.verbose.log_level() { - Some(level) => level.as_str(), - None => "SILENT", - }, - ); - } - let cfg = cfg(cmd.clone()).await?; // write to file @@ -263,17 +219,6 @@ async fn main() -> Result<(), Box> { cmd.transpose_api_key = configuration.transpose_api_key; } - // set logger environment variable if not already set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var( - "RUST_LOG", - match cmd.verbose.log_level() { - Some(level) => level.as_str(), - None => "SILENT", - }, - ); - } - let result = dump(cmd.clone()).await?; let mut lines = Vec::new(); @@ -305,17 +250,6 @@ async fn main() -> Result<(), Box> { cmd.rpc_url = configuration.rpc_url; } - // set logger environment variable if not already set - if std::env::var("RUST_LOG").is_err() { - std::env::set_var( - "RUST_LOG", - match cmd.verbose.log_level() { - Some(level) => level.as_str(), - None => "SILENT", - }, - ); - } - // write to file if ADDRESS_REGEX.is_match(&cmd.target).unwrap() { output_path.push_str(&format!("/{}/snapshot.csv", &cmd.target)); @@ -341,7 +275,7 @@ async fn main() -> Result<(), Box> { } // check if the version is up to date - let remote_version = remote_version(); + let remote_version = remote_version().await; let current_version = current_version(); if remote_version.gt(¤t_version) { diff --git a/common/Cargo.toml b/common/Cargo.toml index 4628808e..978e3424 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -24,3 +24,5 @@ serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" tokio = {version = "1", features = ["full"]} strsim = "0.10.0" +async-recursion = "1.0.5" +async-trait = "0.1.51" diff --git a/common/src/ether/selectors.rs b/common/src/ether/selectors.rs index f0073641..349d241a 100644 --- a/common/src/ether/selectors.rs +++ b/common/src/ether/selectors.rs @@ -138,8 +138,8 @@ where let resolve_progress = resolve_progress.clone(); // create a new thread for each selector - threads.push(thread::spawn(move || { - if let Some(function) = T::resolve(&selector) { + threads.push(thread::spawn(move || async move { + if let Some(function) = T::resolve(&selector).await { let mut _resolved_functions = function_clone.lock().expect("Could not obtain lock on function_clone."); let mut _resolve_progress = diff --git a/common/src/ether/signatures.rs b/common/src/ether/signatures.rs index 2e97ad36..f4c56956 100644 --- a/common/src/ether/signatures.rs +++ b/common/src/ether/signatures.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use ethers::abi::Token; use heimdall_cache::{read_cache, store_cache}; @@ -29,14 +30,16 @@ pub struct ResolvedLog { pub inputs: Vec, } +#[async_trait] pub trait ResolveSelector { - fn resolve(selector: &str) -> Option> + async fn resolve(selector: &str) -> Option> where Self: Sized; } +#[async_trait] impl ResolveSelector for ResolvedError { - fn resolve(selector: &str) -> Option> { + async fn resolve(selector: &str) -> Option> { // get a new logger let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into()); let (logger, _) = Logger::new(&level); @@ -60,7 +63,10 @@ impl ResolveSelector for ResolvedError { let signatures = match get_json_from_url( &format!("https://api.etherface.io/v1/signatures/hash/error/{}/1", &selector), 10, - ) { + ) + .await + .unwrap() + { Some(signatures) => signatures, None => return None, }; @@ -115,8 +121,9 @@ impl ResolveSelector for ResolvedError { } } +#[async_trait] impl ResolveSelector for ResolvedLog { - fn resolve(selector: &str) -> Option> { + async fn resolve(selector: &str) -> Option> { // get a new logger let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into()); let (logger, _) = Logger::new(&level); @@ -140,7 +147,10 @@ impl ResolveSelector for ResolvedLog { let signatures = match get_json_from_url( &format!("https://api.etherface.io/v1/signatures/hash/event/{}/1", &selector), 10, - ) { + ) + .await + .unwrap() + { Some(signatures) => signatures, None => return None, }; @@ -195,8 +205,9 @@ impl ResolveSelector for ResolvedLog { } } +#[async_trait] impl ResolveSelector for ResolvedFunction { - fn resolve(selector: &str) -> Option> { + async fn resolve(selector: &str) -> Option> { // get a new logger let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into()); let (logger, _) = Logger::new(&level); @@ -220,7 +231,10 @@ impl ResolveSelector for ResolvedFunction { let signatures = match get_json_from_url( &format!("https://api.etherface.io/v1/signatures/hash/function/{}/1", &selector), 10, - ) { + ) + .await + .unwrap() + { Some(signatures) => signatures, None => return None, }; diff --git a/common/src/ether/tests.rs b/common/src/ether/tests.rs index 634af467..39b00e93 100644 --- a/common/src/ether/tests.rs +++ b/common/src/ether/tests.rs @@ -610,31 +610,32 @@ mod test_signatures { score_signature, ResolveSelector, ResolvedError, ResolvedFunction, ResolvedLog, }; - #[test] - fn resolve_function_signature_should_return_none_when_cached_results_not_found() { + #[tokio::test] + async fn resolve_function_signature_should_return_none_when_cached_results_not_found() { let signature = String::from("test_signature_nocache"); - let result = ResolvedFunction::resolve(&signature); + let result = ResolvedFunction::resolve(&signature).await; assert_eq!(result, None,) } - #[test] - fn resolve_function_signature_should_return_none_when_json_url_returns_empty_signatures() { + #[tokio::test] + async fn resolve_function_signature_should_return_none_when_json_url_returns_empty_signatures() + { delete_cache(&format!("selector.{}", "test_signature")); let signature = String::from("test_signature"); - let result = ResolvedFunction::resolve(&signature); + let result = ResolvedFunction::resolve(&signature).await; assert_eq!(result, None); } - #[test] - fn resolve_error_signature_should_return_none_when_cached_results_not_found() { + #[tokio::test] + async fn resolve_error_signature_should_return_none_when_cached_results_not_found() { let signature = String::from("test_signature_notfound"); - let result = ResolvedError::resolve(&signature); + let result = ResolvedError::resolve(&signature).await; assert_eq!(result, None); } - #[test] - fn resolve_error_signature_should_return_cached_results_when_found() { + #[tokio::test] + async fn resolve_error_signature_should_return_cached_results_when_found() { let signature = String::from("test_signature"); let mut cached_results = Vec::new(); cached_results.push(ResolvedError { @@ -644,33 +645,33 @@ mod test_signatures { }); store_cache(&format!("selector.{}", &signature), cached_results.clone(), None); - let result = ResolvedError::resolve(&signature); + let result = ResolvedError::resolve(&signature).await; assert_eq!(result, Some(cached_results)); } - #[test] - fn resolve_error_signature_should_return_none_when_json_url_returns_none() { + #[tokio::test] + async fn resolve_error_signature_should_return_none_when_json_url_returns_none() { let signature = String::from("test_signature_notfound"); - let result = ResolvedError::resolve(&signature); + let result = ResolvedError::resolve(&signature).await; assert_eq!(result, None); } - #[test] - fn resolve_error_signature_should_return_none_when_json_url_returns_empty_signatures() { + #[tokio::test] + async fn resolve_error_signature_should_return_none_when_json_url_returns_empty_signatures() { let signature = String::from("test_signature_notfound"); - let result = ResolvedError::resolve(&signature); + let result = ResolvedError::resolve(&signature).await; assert_eq!(result, None); } - #[test] - fn resolve_event_signature_should_return_none_when_cached_results_not_found() { + #[tokio::test] + async fn resolve_event_signature_should_return_none_when_cached_results_not_found() { let signature = String::from("test_signature_notfound"); - let result = ResolvedLog::resolve(&signature); + let result = ResolvedLog::resolve(&signature).await; assert_eq!(result, None); } - #[test] - fn resolve_event_signature_should_return_cached_results_when_found() { + #[tokio::test] + async fn resolve_event_signature_should_return_cached_results_when_found() { let signature = String::from("test_signature"); let mut cached_results = Vec::new(); cached_results.push(ResolvedLog { @@ -680,21 +681,21 @@ mod test_signatures { }); store_cache(&format!("selector.{}", &signature), cached_results.clone(), None); - let result = ResolvedLog::resolve(&signature); + let result = ResolvedLog::resolve(&signature).await; assert_eq!(result, Some(cached_results)); } - #[test] - fn resolve_event_signature_should_return_none_when_json_url_returns_none() { + #[tokio::test] + async fn resolve_event_signature_should_return_none_when_json_url_returns_none() { let signature = String::from("test_signature_notfound"); - let result = ResolvedLog::resolve(&signature); + let result = ResolvedLog::resolve(&signature).await; assert_eq!(result, None); } - #[test] - fn resolve_event_signature_should_return_none_when_json_url_returns_empty_signatures() { + #[tokio::test] + async fn resolve_event_signature_should_return_none_when_json_url_returns_empty_signatures() { let signature = String::from("test_signature_notfound"); - let result = ResolvedLog::resolve(&signature); + let result = ResolvedLog::resolve(&signature).await; assert_eq!(result, None); } diff --git a/common/src/resources/transpose.rs b/common/src/resources/transpose.rs index 2311ad7b..f667c098 100644 --- a/common/src/resources/transpose.rs +++ b/common/src/resources/transpose.rs @@ -1,11 +1,7 @@ -use std::{ - io::Read, - time::{Duration, Instant}, -}; - use indicatif::ProgressBar; use reqwest::header::HeaderMap; use serde_json::Value; +use std::time::{Duration, Instant}; use crate::io::logging::Logger; use serde::{Deserialize, Serialize}; @@ -24,7 +20,7 @@ struct TransposeResponse { results: Vec, } -fn _call_transpose(query: &str, api_key: &str) -> Option { +async fn _call_transpose(query: &str, api_key: &str) -> Option { // get a new logger let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into()); let (logger, _) = Logger::new(&level); @@ -38,17 +34,18 @@ fn _call_transpose(query: &str, api_key: &str) -> Option { let query = query.to_owned(); // make the request - let client = reqwest::blocking::Client::builder() + let client = reqwest::Client::builder() .redirect(reqwest::redirect::Policy::none()) .timeout(Duration::from_secs(999999999)) .build() .unwrap(); - let mut response = match client + let response = match client .post("https://api.transpose.io/sql") .body(query.clone()) .headers(headers) .send() + .await { Ok(res) => res, Err(e) => { @@ -59,9 +56,8 @@ fn _call_transpose(query: &str, api_key: &str) -> Option { }; // parse body - let mut body = String::new(); - match response.read_to_string(&mut body) { - Ok(_) => Some(match serde_json::from_str(&body) { + match response.text().await { + Ok(body) => Some(match serde_json::from_str(&body) { Ok(json) => json, Err(e) => { logger.error("Transpose request unsucessful."); @@ -74,13 +70,12 @@ fn _call_transpose(query: &str, api_key: &str) -> Option { Err(e) => { logger.error("failed to parse Transpose response body."); logger.error(&format!("error: {e}")); - logger.debug(&format!("response body: {body:?}")); std::process::exit(1) } } } -pub fn get_transaction_list( +pub async fn get_transaction_list( chain: &str, address: &str, api_key: &str, @@ -108,7 +103,7 @@ pub fn get_transaction_list( bounds.1 ); - let response = match _call_transpose(&query, api_key) { + let response = match _call_transpose(&query, api_key).await { Some(response) => response, None => { logger.error("failed to get transaction list from Transpose"); @@ -159,7 +154,11 @@ pub fn get_transaction_list( transactions } -pub fn get_contract_creation(chain: &str, address: &str, api_key: &str) -> Option<(u128, String)> { +pub async fn get_contract_creation( + chain: &str, + address: &str, + api_key: &str, +) -> Option<(u128, String)> { // get a new logger let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into()); let (logger, _) = Logger::new(&level); @@ -176,7 +175,7 @@ pub fn get_contract_creation(chain: &str, address: &str, api_key: &str) -> Optio "{{\"sql\":\"SELECT block_number, transaction_hash FROM {chain}.transactions WHERE TIMESTAMP = ( SELECT created_timestamp FROM {chain}.accounts WHERE address = '{address}' ) AND contract_address = '{address}'\",\"parameters\":{{}},\"options\":{{\"timeout\": 999999999}}}}", ); - let response = match _call_transpose(&query, api_key) { + let response = match _call_transpose(&query, api_key).await { Some(response) => response, None => { logger.error("failed to get creation tx from Transpose"); diff --git a/common/src/utils/http.rs b/common/src/utils/http.rs index 6617cf66..0f9e2ca2 100644 --- a/common/src/utils/http.rs +++ b/common/src/utils/http.rs @@ -1,59 +1,55 @@ -use reqwest::blocking::Client; +use async_recursion::async_recursion; +use reqwest::Client; use serde_json::Value; -use std::io::Read; +use std::time::Duration; +use tokio::time::sleep as async_sleep; static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); -/// make a GET request to the target URL and return the response body as JSON +/// Make a GET request to the target URL and return the response body as JSON /// /// # Arguments /// `url` - the URL to make the GET request to +/// `timeout` - timeout duration in seconds /// /// # Returns -/// `Option` - the response body as JSON -pub fn get_json_from_url(url: &str, timeout: u64) -> Option { - _get_json_from_url(url, 0, 5, timeout) +/// `Result, reqwest::Error>` - the response body as JSON +pub async fn get_json_from_url(url: &str, timeout: u64) -> Result, reqwest::Error> { + _get_json_from_url(url, 0, 5, timeout).await } -fn _get_json_from_url( +#[async_recursion] +async fn _get_json_from_url( url: &str, retry_count: u8, retries_remaining: u8, timeout: u64, -) -> Option { - let client = match Client::builder() +) -> Result, reqwest::Error> { + let client = Client::builder() .danger_accept_invalid_certs(true) .user_agent(APP_USER_AGENT) - .timeout(std::time::Duration::from_secs(timeout)) - .build() - { - Ok(client) => client, - Err(_) => Client::default(), - }; + .timeout(Duration::from_secs(timeout)) + .build()?; - let mut res = match client.get(url).send() { + let res = match client.get(url).send().await { Ok(res) => res, Err(_) => { if retries_remaining == 0 { - return None + return Ok(None) } // exponential backoff let retry_count = retry_count + 1; let retries_remaining = retries_remaining - 1; - let sleep_time = 2u64.pow(retry_count as u32) * 250; - std::thread::sleep(std::time::Duration::from_millis(sleep_time)); - return _get_json_from_url(url, retry_count, retries_remaining, timeout) + async_sleep(Duration::from_millis(sleep_time)).await; + return _get_json_from_url(url, retry_count, retries_remaining, timeout).await } }; - let mut body = String::new(); + let body = res.text().await?; - match res.read_to_string(&mut body) { - Ok(_) => Some(match serde_json::from_str(&body) { - Ok(json) => json, - Err(_) => return None, - }), - Err(_) => None, + match serde_json::from_str(&body) { + Ok(json) => Ok(Some(json)), + Err(_) => Ok(None), } } diff --git a/common/src/utils/tests.rs b/common/src/utils/tests.rs index 453e23b8..b5c69f78 100644 --- a/common/src/utils/tests.rs +++ b/common/src/utils/tests.rs @@ -548,9 +548,9 @@ mod test_version { assert_eq!(version.to_string(), env!("CARGO_PKG_VERSION")); } - #[test] - fn test_version_remote() { - let version = remote_version(); + #[tokio::test] + async fn test_version_remote() { + let version = remote_version().await; assert!(version.minor >= 0); assert!(version.patch >= 0); diff --git a/common/src/utils/version.rs b/common/src/utils/version.rs index 2dddac3d..b5539a27 100644 --- a/common/src/utils/version.rs +++ b/common/src/utils/version.rs @@ -21,13 +21,13 @@ pub fn current_version() -> Version { } } -pub fn remote_version() -> Version { +pub async fn remote_version() -> Version { // get the latest release from github let remote_repository_url = "https://api.github.com/repos/Jon-Becker/heimdall-rs/releases/latest"; // retrieve the latest release tag from github - if let Some(release) = get_json_from_url(remote_repository_url, 1) { + if let Some(release) = get_json_from_url(remote_repository_url, 1).await.unwrap() { if let Some(tag_name) = release["tag_name"].as_str() { let version_string = tag_name.replace('v', ""); let version_parts: Vec<&str> = version_string.split('.').collect(); diff --git a/core/Cargo.toml b/core/Cargo.toml index bdcf41a3..ff233ebc 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,3 +26,4 @@ serde_json = "1.0" strsim = "0.10.0" tokio = {version = "1", features = ["full"]} tui = "0.19" +derive_builder = "0.12.0" diff --git a/core/src/cfg/mod.rs b/core/src/cfg/mod.rs index 3d477585..9bc73c99 100644 --- a/core/src/cfg/mod.rs +++ b/core/src/cfg/mod.rs @@ -1,5 +1,6 @@ pub mod graph; pub mod output; +use derive_builder::Builder; use heimdall_common::ether::{ compiler::detect_compiler, rpc::get_code, selectors::find_function_selectors, }; @@ -19,7 +20,7 @@ use petgraph::Graph; use crate::cfg::graph::build_cfg; -#[derive(Debug, Clone, Parser)] +#[derive(Debug, Clone, Parser, Builder)] #[clap( about = "Generate a visual control flow graph for EVM bytecode", after_help = "For more information, read the wiki: https://jbecker.dev/r/heimdall-rs/wiki", @@ -54,10 +55,34 @@ pub struct CFGArgs { pub color_edges: bool, } +impl CFGArgsBuilder { + pub fn new() -> Self { + Self { + target: Some(String::new()), + verbose: Some(clap_verbosity_flag::Verbosity::new(0, 1)), + rpc_url: Some(String::new()), + default: Some(true), + format: Some(String::new()), + color_edges: Some(false), + } + } +} + pub async fn cfg(args: CFGArgs) -> Result, Box> { use std::time::Instant; let now = Instant::now(); + // set logger environment variable if not already set + if std::env::var("RUST_LOG").is_err() { + std::env::set_var( + "RUST_LOG", + match args.verbose.log_level() { + Some(level) => level.as_str(), + None => "SILENT", + }, + ); + } + let (logger, mut trace) = Logger::new(match args.verbose.log_level() { Some(level) => level.as_str(), None => "SILENT", diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 65c122f3..e3fa7d2d 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -3,6 +3,7 @@ mod util; use std::time::Duration; use clap::{AppSettings, Parser}; +use derive_builder::Builder; use ethers::{ abi::{decode as decode_abi, AbiEncode, Function, Param, ParamType, StateMutability}, types::Transaction, @@ -24,7 +25,7 @@ use strsim::normalized_damerau_levenshtein as similarity; use crate::decode::util::get_explanation; -#[derive(Debug, Clone, Parser)] +#[derive(Debug, Clone, Parser, Builder)] #[clap( about = "Decode calldata into readable types", after_help = "For more information, read the wiki: https://jbecker.dev/r/heimdall-rs/wiki", @@ -61,12 +62,40 @@ pub struct DecodeArgs { pub truncate_calldata: bool, } +impl DecodeArgsBuilder { + pub fn new() -> Self { + Self { + target: Some(String::new()), + verbose: Some(clap_verbosity_flag::Verbosity::new(0, 0)), + rpc_url: Some(String::new()), + openai_api_key: Some(String::new()), + explain: Some(false), + default: Some(true), + truncate_calldata: Some(false), + } + } +} + #[allow(deprecated)] -pub async fn decode(args: DecodeArgs) { +pub async fn decode(args: DecodeArgs) -> Result, Box> { + // set logger environment variable if not already set + if std::env::var("RUST_LOG").is_err() { + std::env::set_var( + "RUST_LOG", + match args.verbose.log_level() { + Some(level) => level.as_str(), + None => "SILENT", + }, + ); + } + + // get a new logger let (logger, mut trace) = Logger::new(match args.verbose.log_level() { Some(level) => level.as_str(), None => "SILENT", }); + + // init variables let mut raw_transaction: Transaction = Transaction::default(); let calldata; @@ -130,7 +159,7 @@ pub async fn decode(args: DecodeArgs) { }; // get the function signature possibilities - let potential_matches = match ResolvedFunction::resolve(&function_selector) { + let potential_matches = match ResolvedFunction::resolve(&function_selector).await { Some(signatures) => signatures, None => Vec::new(), }; @@ -400,121 +429,6 @@ pub async fn decode(args: DecodeArgs) { }; } } -} - -/// Decode calldata into a Vec of potential ResolvedFunctions -/// ## Example -/// ```no_run -/// # use crate::heimdall_core::decode::decode_calldata; -/// const CALLDATA: &'static str = "0xd57e/* snip */2867" -/// -/// let potential_matches = decode_calldata(CALLDATA.to_string()); -#[allow(deprecated)] -#[allow(dead_code)] -pub fn decode_calldata(calldata: &str) -> Option> { - let (logger, _) = Logger::new("ERROR"); - - // parse the two parts of calldata, inputs and selector - let function_selector = - calldata.replacen("0x", "", 1).get(0..8).unwrap_or("0x00000000").to_string(); - let byte_args = match decode_hex(&calldata[8..]) { - Ok(byte_args) => byte_args, - Err(_) => { - logger.error("failed to parse bytearray from calldata."); - std::process::exit(1) - } - }; - - // get the function signature possibilities - let potential_matches = match ResolvedFunction::resolve(&function_selector) { - Some(signatures) => signatures, - None => Vec::new(), - }; - let mut matches: Vec = Vec::new(); - for potential_match in &potential_matches { - // convert the string inputs into a vector of decoded types - let mut inputs: Vec = Vec::new(); - - if let Some(type_) = parse_function_parameters(&potential_match.signature) { - for input in type_ { - inputs.push(input); - } - } - - match decode_abi(&inputs, &byte_args) { - Ok(result) => { - // convert tokens to params - let mut params: Vec = Vec::new(); - for (i, input) in inputs.iter().enumerate() { - params.push(Param { - name: format!("arg{i}"), - kind: input.to_owned(), - internal_type: None, - }); - } - // build the decoded function to verify it's a match - let decoded_function_call = Function { - name: potential_match.name.to_string(), - inputs: params, - outputs: Vec::new(), - constant: None, - state_mutability: StateMutability::NonPayable, - } - .encode_input(&result); - match decoded_function_call { - Ok(decoded_function_call) => { - // decode the function call in trimmed bytes, removing 0s, because contracts - // can use nonstandard sized words and padding is - // hard - let cleaned_bytes = decoded_function_call.encode_hex().replace('0', ""); - let decoded_function_call = match cleaned_bytes - .split_once(&function_selector.replace('0', "")) - { - Some(decoded_function_call) => decoded_function_call.1, - None => { - logger.debug(&format!("potential match '{}' ignored. decoded inputs differed from provided calldata.", &potential_match.signature).to_string()); - continue - } - }; - - // if the decoded function call matches (95%) the function signature, add it - // to the list of matches - if similarity(decoded_function_call, &calldata[8..].replace('0', "")).abs() >= - 0.90 - { - let mut found_match = potential_match.clone(); - found_match.decoded_inputs = Some(result); - matches.push(found_match); - } else { - logger.debug(&format!("potential match '{}' ignored. decoded inputs differed from provided calldata.", &potential_match.signature).to_string()); - } - } - Err(_) => { - logger.debug( - &format!( - "potential match '{}' ignored. type checking failed", - &potential_match.signature - ) - .to_string(), - ); - } - } - } - Err(_) => { - logger.debug( - &format!( - "potential match '{}' ignored. decoding types failed", - &potential_match.signature - ) - .to_string(), - ); - } - } - } - - if matches.is_empty() { - return None - } - Some(matches) + Ok(matches) } diff --git a/core/src/decompile/mod.rs b/core/src/decompile/mod.rs index 686eb590..c6e8c826 100644 --- a/core/src/decompile/mod.rs +++ b/core/src/decompile/mod.rs @@ -12,6 +12,7 @@ use crate::decompile::{ util::*, }; +use derive_builder::Builder; use heimdall_common::{ ether::{ compiler::detect_compiler, @@ -38,7 +39,7 @@ use heimdall_common::{ use self::out::abi::ABIStructure; -#[derive(Debug, Clone, Parser)] +#[derive(Debug, Clone, Parser, Builder)] #[clap( about = "Decompile EVM bytecode to Solidity", after_help = "For more information, read the wiki: https://jbecker.dev/r/heimdall-rs/wiki", @@ -75,6 +76,20 @@ pub struct DecompilerArgs { pub include_yul: bool, } +impl DecompilerArgsBuilder { + pub fn new() -> Self { + Self { + target: Some(String::new()), + verbose: Some(clap_verbosity_flag::Verbosity::new(0, 1)), + rpc_url: Some(String::new()), + default: Some(true), + skip_resolving: Some(false), + include_solidity: Some(false), + include_yul: Some(false), + } + } +} + #[derive(Debug, Clone)] pub struct DecompileResult { pub source: Option, @@ -87,9 +102,22 @@ pub async fn decompile( use std::time::Instant; let now = Instant::now(); + // set logger environment variable if not already set + if std::env::var("RUST_LOG").is_err() { + std::env::set_var( + "RUST_LOG", + match args.verbose.log_level() { + Some(level) => level.as_str(), + None => "SILENT", + }, + ); + } + // get a new logger - let level = std::env::var("RUST_LOG").unwrap_or_else(|_| "INFO".into()); - let (logger, mut trace) = Logger::new(&level); + let (logger, mut trace) = Logger::new(match args.verbose.log_level() { + Some(level) => level.as_str(), + None => "SILENT", + }); let mut all_resolved_events: HashMap = HashMap::new(); let mut all_resolved_errors: HashMap = HashMap::new(); diff --git a/core/src/dump/mod.rs b/core/src/dump/mod.rs index 3ac9f855..0c7d83f5 100644 --- a/core/src/dump/mod.rs +++ b/core/src/dump/mod.rs @@ -4,6 +4,7 @@ mod structures; mod util; use clap::{AppSettings, Parser}; +use derive_builder::Builder; use ethers::types::H160; use heimdall_common::{ io::logging::*, @@ -18,7 +19,7 @@ use self::{ util::csv::{build_csv, DumpRow}, }; -#[derive(Debug, Clone, Parser)] +#[derive(Debug, Clone, Parser, Builder)] #[clap( about = "Dump the value of all storage slots accessed by a contract", after_help = "For more information, read the wiki: https://jbecker.dev/r/heimdall-rs/wiki", @@ -68,7 +69,35 @@ pub struct DumpArgs { pub chain: String, } +impl DumpArgsBuilder { + pub fn new() -> Self { + Self { + target: Some(String::new()), + verbose: Some(clap_verbosity_flag::Verbosity::new(0, 1)), + output: Some(String::new()), + rpc_url: Some(String::new()), + transpose_api_key: Some(String::new()), + threads: Some(8), + from_block: Some(0), + to_block: Some(9999999999), + no_tui: Some(true), + chain: Some(String::from("ethereum")), + } + } +} + pub async fn dump(args: DumpArgs) -> Result, Box> { + // set logger environment variable if not already set + if std::env::var("RUST_LOG").is_err() { + std::env::set_var( + "RUST_LOG", + match args.verbose.log_level() { + Some(level) => level.as_str(), + None => "SILENT", + }, + ); + } + let (logger, _) = Logger::new(match args.verbose.log_level() { Some(level) => level.as_str(), None => "SILENT", @@ -96,7 +125,7 @@ pub async fn dump(args: DumpArgs) -> Result, Box tx, None => { logger.error( @@ -134,7 +163,8 @@ pub async fn dump(args: DumpArgs) -> Result, Box Self { + SnapshotArgsBuilder { + target: Some(String::new()), + verbose: Some(clap_verbosity_flag::Verbosity::new(0, 1)), + rpc_url: Some(String::new()), + default: Some(true), + skip_resolving: Some(false), + no_tui: Some(true), + } + } +} + +#[derive(Debug, Clone)] pub struct SnapshotResult { pub snapshots: Vec, pub resolved_errors: HashMap, @@ -78,6 +93,17 @@ pub async fn snapshot(args: SnapshotArgs) -> Result level.as_str(), + None => "SILENT", + }, + ); + } + let (logger, mut trace) = Logger::new(match args.verbose.log_level() { Some(level) => level.as_str(), None => "SILENT", diff --git a/examples/cfg/Cargo.toml b/examples/cfg/Cargo.toml new file mode 100644 index 00000000..7d8a5d52 --- /dev/null +++ b/examples/cfg/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "heimdall-rs-cfg-example" +version = "0.1.0" +edition = "2021" +description = "Heimdall is an advanced Ethereum smart contract toolkit for forensic and heuristic analysis." +keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"] +license = "MIT" + +[dependencies] +# heimdall-core = { git = "https://github.com/Jon-Becker/heimdall-rs.git", branch = "main" } +heimdall-core = { path = "../../core" } +tokio = {version = "1", features = ["full"]} diff --git a/examples/cfg/src/main.rs b/examples/cfg/src/main.rs new file mode 100644 index 00000000..38ae6a9a --- /dev/null +++ b/examples/cfg/src/main.rs @@ -0,0 +1,16 @@ +use heimdall_core::cfg::CFGArgsBuilder; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let result = heimdall_core::cfg::cfg( + CFGArgsBuilder::new() + .target("0x9f00c43700bc0000Ff91bE00841F8e04c0495000".to_string()) + .rpc_url("https://eth.llamarpc.com".to_string()) + .build()?, + ) + .await?; + + println!("Contract CFG: {:#?}", result); + + Ok(()) +} diff --git a/examples/decode/Cargo.toml b/examples/decode/Cargo.toml new file mode 100644 index 00000000..78c273d5 --- /dev/null +++ b/examples/decode/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "heimdall-rs-decode-example" +version = "0.1.0" +edition = "2021" +description = "Heimdall is an advanced Ethereum smart contract toolkit for forensic and heuristic analysis." +keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"] +license = "MIT" + +[dependencies] +# heimdall-core = { git = "https://github.com/Jon-Becker/heimdall-rs.git", branch = "main" } +heimdall-core = { path = "../../core" } +tokio = {version = "1", features = ["full"]} diff --git a/examples/decode/src/main.rs b/examples/decode/src/main.rs new file mode 100644 index 00000000..68dcfce8 --- /dev/null +++ b/examples/decode/src/main.rs @@ -0,0 +1,14 @@ +use heimdall_core::decode::DecodeArgsBuilder; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _ = heimdall_core::decode::decode( + DecodeArgsBuilder::new() + .target("0x9f00c43700bc0000Ff91bE00841F8e04c0495000".to_string()) + .rpc_url("https://eth.llamarpc.com".to_string()) + .build()?, + ) + .await?; + + Ok(()) +} diff --git a/examples/decompile/Cargo.toml b/examples/decompile/Cargo.toml new file mode 100644 index 00000000..72804496 --- /dev/null +++ b/examples/decompile/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "heimdall-rs-decompile-example" +version = "0.1.0" +edition = "2021" +description = "Heimdall is an advanced Ethereum smart contract toolkit for forensic and heuristic analysis." +keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"] +license = "MIT" + +[dependencies] +# heimdall-core = { git = "https://github.com/Jon-Becker/heimdall-rs.git", branch = "main" } +heimdall-core = { path = "../../core" } +tokio = {version = "1", features = ["full"]} diff --git a/examples/decompile/src/main.rs b/examples/decompile/src/main.rs new file mode 100644 index 00000000..17a0f0b9 --- /dev/null +++ b/examples/decompile/src/main.rs @@ -0,0 +1,16 @@ +use heimdall_core::decompile::DecompilerArgsBuilder; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let result = heimdall_core::decompile::decompile( + DecompilerArgsBuilder::new() + .target("0x9f00c43700bc0000Ff91bE00841F8e04c0495000".to_string()) + .rpc_url("https://eth.llamarpc.com".to_string()) + .build()?, + ) + .await?; + + println!("Decompiled contract: {:#?}", result); + + Ok(()) +} diff --git a/examples/dump/Cargo.toml b/examples/dump/Cargo.toml new file mode 100644 index 00000000..fcf5530c --- /dev/null +++ b/examples/dump/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "heimdall-rs-dump-example" +version = "0.1.0" +edition = "2021" +description = "Heimdall is an advanced Ethereum smart contract toolkit for forensic and heuristic analysis." +keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"] +license = "MIT" + +[dependencies] +# heimdall-core = { git = "https://github.com/Jon-Becker/heimdall-rs.git", branch = "main" } +heimdall-core = { path = "../../core" } +tokio = {version = "1", features = ["full"]} diff --git a/examples/dump/src/main.rs b/examples/dump/src/main.rs new file mode 100644 index 00000000..a416e17f --- /dev/null +++ b/examples/dump/src/main.rs @@ -0,0 +1,19 @@ +use heimdall_core::dump::DumpArgsBuilder; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let result = heimdall_core::dump::dump( + DumpArgsBuilder::new() + .target("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".to_string()) + .rpc_url("https://eth.llamarpc.com".to_string()) + .transpose_api_key("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string()) + .from_block(10000000) + .to_block(10000001) + .build()?, + ) + .await?; + + println!("Contract Storage: {:#?}", result); + + Ok(()) +} diff --git a/examples/snapshot/Cargo.toml b/examples/snapshot/Cargo.toml new file mode 100644 index 00000000..c16ccd06 --- /dev/null +++ b/examples/snapshot/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "heimdall-rs-snapshot-example" +version = "0.1.0" +edition = "2021" +description = "Heimdall is an advanced Ethereum smart contract toolkit for forensic and heuristic analysis." +keywords = ["ethereum", "web3", "decompiler", "evm", "crypto"] +license = "MIT" + +[dependencies] +# heimdall-core = { git = "https://github.com/Jon-Becker/heimdall-rs.git", branch = "main" } +heimdall-core = { path = "../../core" } +tokio = {version = "1", features = ["full"]} diff --git a/examples/snapshot/src/main.rs b/examples/snapshot/src/main.rs new file mode 100644 index 00000000..0955db67 --- /dev/null +++ b/examples/snapshot/src/main.rs @@ -0,0 +1,16 @@ +use heimdall_core::snapshot::SnapshotArgsBuilder; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let result = heimdall_core::snapshot::snapshot( + SnapshotArgsBuilder::new() + .target("0x9f00c43700bc0000Ff91bE00841F8e04c0495000".to_string()) + .rpc_url("https://eth.llamarpc.com".to_string()) + .build()?, + ) + .await?; + + println!("Snapshot contract: {:#?}", result); + + Ok(()) +}