From fadfd04ea44b3676ad7ab65e5d7e087533bfe43a Mon Sep 17 00:00:00 2001 From: Jonathan Becker Date: Sun, 31 Dec 2023 18:14:41 -0600 Subject: [PATCH] chore: remove panicky `unwrap()`s, use `Result<(), ..> (#261) * chore(cache): remove panicky `unwrap()`s, use `Result<(), ..> * chore: remove panicky `unwrap()`s, use `Result<(), ..> * fix(doctests): cache doctests --- Cargo.lock | 2 + cache/Cargo.toml | 1 + cache/src/error.rs | 7 ++ cache/src/lib.rs | 177 ++++++++++++++++++++------------- common/Cargo.toml | 1 + common/src/error.rs | 7 ++ common/src/ether/bytecode.rs | 26 +++-- common/src/ether/rpc.rs | 92 ++++++++--------- common/src/ether/selectors.rs | 2 +- common/src/ether/signatures.rs | 111 +++++++++++---------- common/src/lib.rs | 1 + core/src/decode/mod.rs | 40 +++++--- core/src/inspect/core/logs.rs | 7 +- 13 files changed, 275 insertions(+), 199 deletions(-) create mode 100644 cache/src/error.rs create mode 100644 common/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 5351b015..f8d8bef5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1657,6 +1657,7 @@ dependencies = [ "clap", "serde", "serde_json", + "thiserror", ] [[package]] @@ -1701,6 +1702,7 @@ dependencies = [ "serde", "serde_json", "strsim", + "thiserror", "tokio", ] diff --git a/cache/Cargo.toml b/cache/Cargo.toml index 245f521a..fc528f35 100644 --- a/cache/Cargo.toml +++ b/cache/Cargo.toml @@ -12,3 +12,4 @@ clap = { version = "3.1.18", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } bincode = "1.3.3" serde_json = "1.0.108" +thiserror = "1.0.50" diff --git a/cache/src/error.rs b/cache/src/error.rs new file mode 100644 index 00000000..9e17975b --- /dev/null +++ b/cache/src/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Error: {0}")] + Generic(String), + #[error("IO error: {0}")] + IOError(String), +} diff --git a/cache/src/lib.rs b/cache/src/lib.rs index da7fc3bd..55579b62 100644 --- a/cache/src/lib.rs +++ b/cache/src/lib.rs @@ -3,8 +3,10 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; #[allow(deprecated)] use std::env::home_dir; +use error::Error; use util::*; +pub mod error; pub mod util; /// Clap argument parser for the cache subcommand @@ -59,24 +61,36 @@ pub struct Cache { /// store_cache("clear_cache_key", "value", None); /// /// /// assert that the cache contains the key -/// assert!(keys("*").contains(&"clear_cache_key".to_string())); +/// assert!(keys("*").expect("!").contains(&"clear_cache_key".to_string())); /// /// /// clear the cache /// clear_cache(); /// /// /// assert that the cache no longer contains the key -/// assert!(!keys("*").contains(&"clear_cache_key".to_string())); +/// assert!(!keys("*").expect("!").contains(&"clear_cache_key".to_string())); /// ``` #[allow(deprecated)] -pub fn clear_cache() { - let home = home_dir().unwrap(); +pub fn clear_cache() -> Result<(), Error> { + let home = home_dir().ok_or(Error::Generic( + "failed to get home directory. does your os support `std::env::home_dir()`?".to_string(), + ))?; let cache_dir = home.join(".bifrost").join("cache"); - for entry in cache_dir.read_dir().unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - delete_path(path.to_str().unwrap()); + for entry in cache_dir + .read_dir() + .map_err(|e| Error::Generic(format!("failed to read cache directory: {:?}", e)))? + { + let entry = + entry.map_err(|e| Error::Generic(format!("failed to read cache entry: {:?}", e)))?; + delete_path( + entry + .path() + .to_str() + .ok_or(Error::Generic("failed to convert path to string".to_string()))?, + ); } + + Ok(()) } /// Check if a cached object exists @@ -88,18 +102,20 @@ pub fn clear_cache() { /// store_cache("exists_key", "value", None); /// /// /// assert that the cache contains the key -/// assert!(exists("exists_key")); +/// assert!(exists("exists_key").expect("!")); /// /// /// assert that the cache does not contain a non-existent key -/// assert!(!exists("non_existent_key")); +/// assert!(!exists("non_existent_key").expect("!")); /// ``` #[allow(deprecated)] -pub fn exists(key: &str) -> bool { - let home = home_dir().unwrap(); +pub fn exists(key: &str) -> Result { + let home = home_dir().ok_or(Error::Generic( + "failed to get home directory. does your os support `std::env::home_dir()`?".to_string(), + ))?; let cache_dir = home.join(".bifrost").join("cache"); let cache_file = cache_dir.join(format!("{key}.bin")); - cache_file.exists() + Ok(cache_file.exists()) } /// List all cached objects @@ -111,27 +127,38 @@ pub fn exists(key: &str) -> bool { /// store_cache("keys_key", "value", None); /// /// /// assert that the cache contains the key -/// assert!(keys("*").contains(&"keys_key".to_string())); +/// assert!(keys("*").expect("!").contains(&"keys_key".to_string())); /// /// /// assert that the cache does not contain a non-existent key -/// assert!(!keys("*").contains(&"non_existent_key".to_string())); +/// assert!(!keys("*").expect("!").contains(&"non_existent_key".to_string())); /// /// /// assert that the cache contains the key -/// assert!(keys("keys_*").contains(&"keys_key".to_string())); +/// assert!(keys("keys_*").expect("!").contains(&"keys_key".to_string())); /// ``` #[allow(deprecated)] -pub fn keys(pattern: &str) -> Vec { - let home = home_dir().unwrap(); +pub fn keys(pattern: &str) -> Result, Error> { + let home = home_dir().ok_or(Error::Generic( + "failed to get home directory. does your os support `std::env::home_dir()`?".to_string(), + ))?; let cache_dir = home.join(".bifrost").join("cache"); let mut keys = Vec::new(); // remove wildcard let pattern = pattern.replace('*', ""); - for entry in cache_dir.read_dir().unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let key = path.file_name().unwrap().to_str().unwrap().to_string(); + for entry in cache_dir + .read_dir() + .map_err(|e| Error::Generic(format!("failed to read cache directory: {:?}", e)))? + { + let entry = + entry.map_err(|e| Error::Generic(format!("failed to read cache entry: {:?}", e)))?; + let key = entry + .path() + .file_name() + .ok_or(Error::Generic("failed to get file name".to_string()))? + .to_str() + .ok_or(Error::Generic("failed to convert path to string".to_string()))? + .to_string(); if pattern.is_empty() || key.contains(&pattern) { keys.push(key.replace(".bin", "")); } @@ -140,7 +167,7 @@ pub fn keys(pattern: &str) -> Vec { // sort keys alphabetically keys.sort(); - keys + Ok(keys) } /// Delete a cached object @@ -151,23 +178,28 @@ pub fn keys(pattern: &str) -> Vec { /// store_cache("delete_cache_key", "value", None); /// /// /// assert that the cache contains the key -/// assert!(keys("*").contains(&"delete_cache_key".to_string())); +/// assert!(keys("*").expect("!").contains(&"delete_cache_key".to_string())); /// /// /// delete the cached object /// delete_cache("delete_cache_key"); /// /// /// assert that the cache does not contain the key -/// assert!(!keys("*").contains(&"delete_cache_key".to_string())); +/// assert!(!keys("*").expect("!").contains(&"delete_cache_key".to_string())); /// ``` #[allow(deprecated)] -pub fn delete_cache(key: &str) { - let home = home_dir().unwrap(); +pub fn delete_cache(key: &str) -> Result<(), Error> { + let home = home_dir().ok_or(Error::Generic( + "failed to get home directory. does your os support `std::env::home_dir()`?".to_string(), + ))?; let cache_dir = home.join(".bifrost").join("cache"); let cache_file = cache_dir.join(format!("{key}.bin")); if cache_file.exists() { - std::fs::remove_file(cache_file).unwrap(); + std::fs::remove_file(cache_file) + .map_err(|e| Error::IOError(format!("failed to delete cache file: {:?}", e)))?; } + + Ok(()) } /// Read a cached object @@ -179,41 +211,45 @@ pub fn delete_cache(key: &str) { /// store_cache("read_cache_key", "value", None); /// /// /// read the cached object -/// assert_eq!(read_cache::("read_cache_key").unwrap(), "value"); +/// assert_eq!(read_cache::("read_cache_key").expect("!").expect("!"), "value"); /// ``` #[allow(deprecated)] -pub fn read_cache(key: &str) -> Option +pub fn read_cache(key: &str) -> Result, Error> where T: 'static + DeserializeOwned, { - let home = home_dir().unwrap(); + let home = home_dir().ok_or(Error::Generic( + "failed to get home directory. does your os support `std::env::home_dir()`?".to_string(), + ))?; let cache_dir = home.join(".bifrost").join("cache"); let cache_file = cache_dir.join(format!("{key}.bin")); - let binary_string = match read_file(cache_file.to_str().unwrap()) { + let binary_string = match read_file( + cache_file + .to_str() + .ok_or(Error::Generic("failed to convert path to string".to_string()))?, + ) { Some(s) => s, - None => return None, + None => return Ok(None), }; - let binary_vec = decode_hex(&binary_string).ok()?; - - let cache: Cache = match bincode::deserialize::>(&binary_vec) { - Ok(c) => { - // check if the cache has expired, if so, delete it and return None - if c.expiry < - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs() - { - delete_cache(key); - return None - } + let binary_vec = decode_hex(&binary_string) + .map_err(|e| Error::Generic(format!("failed to decode hex: {:?}", e)))?; + + let cache: Cache = bincode::deserialize::>(&binary_vec) + .map_err(|e| Error::Generic(format!("failed to deserialize cache object: {:?}", e)))?; + + // check if the cache has expired, if so, delete it and return None + if cache.expiry < + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| Error::Generic(format!("failed to get current time: {:?}", e)))? + .as_secs() + { + delete_cache(key)?; + return Ok(None); + } - c - } - Err(_) => return None, - }; - Some(*Box::new(cache.value)) + Ok(Some(*Box::new(cache.value))) } /// Store a value in the cache, with an optional expiry time \ @@ -229,26 +265,27 @@ where /// store_cache("store_cache_key2", "value", Some(60 * 60 * 24)); /// ``` #[allow(deprecated)] -pub fn store_cache( - key: &str, - value: T, - expiry: Option, -) -> Result<(), Box> +pub fn store_cache(key: &str, value: T, expiry: Option) -> Result<(), Error> where T: Serialize, { - let home = home_dir().unwrap(); + let home = home_dir().ok_or(Error::Generic( + "failed to get home directory. does your os support `std::env::home_dir()`?".to_string(), + ))?; let cache_dir = home.join(".bifrost").join("cache"); let cache_file = cache_dir.join(format!("{key}.bin")); // expire in 90 days let expiry = expiry.unwrap_or( - std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() + + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|e| Error::Generic(format!("failed to get current time: {:?}", e)))? + .as_secs() + 60 * 60 * 24 * 90, ); let cache = Cache { value, expiry }; let encoded: Vec = bincode::serialize(&cache) - .map_err(|e| format!("Failed to serialize cache object: {:?}", e))?; + .map_err(|e| Error::Generic(format!("failed to serialize cache object: {:?}", e)))?; let binary_string = encode_hex(encoded); write_file(cache_file.to_str().unwrap(), &binary_string); @@ -257,14 +294,14 @@ where /// Cache subcommand handler #[allow(deprecated)] -pub fn cache(args: CacheArgs) -> Result<(), Box> { +pub fn cache(args: CacheArgs) -> Result<(), Error> { match args.sub { Subcommands::Clean(_) => { - clear_cache(); + clear_cache()?; println!("Cache cleared.") } Subcommands::Ls(_) => { - let keys = keys("*"); + let keys = keys("*")?; println!("Displaying {} cached objects:", keys.len()); for (i, key) in keys.iter().enumerate() { @@ -283,7 +320,7 @@ pub fn cache(args: CacheArgs) -> Result<(), Box> { size += metadata.len(); } - println!("Cached objects: {}", keys("*").len()); + println!("Cached objects: {}", keys("*")?.len()); println!("Cache size: {}", prettify_bytes(size)); } } @@ -314,7 +351,7 @@ mod tests { fn test_get_cache() { store_cache("key3", "value".to_string(), None); let value = read_cache("key3"); - let value: String = value.unwrap(); + let value: String = value.unwrap().unwrap(); // assert stored value matches assert_eq!(value, "value"); @@ -351,7 +388,7 @@ mod tests { store_cache("struct2", test_struct, None); let value = read_cache("struct2"); - let value: TestStruct = value.unwrap(); + let value: TestStruct = value.unwrap().unwrap(); // assert stored value matches assert_eq!(value.name, "test"); @@ -364,7 +401,7 @@ mod tests { store_cache("some_other_key", "some_value", None); store_cache("not_a_key", "some_value", None); - assert_eq!(keys("some_"), vec!["some_key", "some_other_key"]); + assert_eq!(keys("some_").unwrap(), vec!["some_key", "some_other_key"]); } #[test] @@ -378,14 +415,14 @@ mod tests { assert!(["a", "b", "c", "d", "e", "f"] .iter() - .all(|key| { keys("*").contains(&key.to_string()) })); + .all(|key| { keys("*").unwrap().contains(&key.to_string()) })); } #[test] fn test_exists() { - assert!(!exists("does_not_exist")); + assert!(!exists("does_not_exist").unwrap()); store_cache("does_not_exist", "some_value", None); - assert!(exists("does_not_exist")); + assert!(exists("does_not_exist").unwrap()); delete_cache("does_not_exist"); } } diff --git a/common/Cargo.toml b/common/Cargo.toml index b0679fa4..3ef725a7 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -28,3 +28,4 @@ async-recursion = "1.0.5" async-trait = "0.1.51" chrono = "0.4.31" backoff = {version = "0.4.0", features = ["tokio"]} +thiserror = "1.0.50" diff --git a/common/src/error.rs b/common/src/error.rs new file mode 100644 index 00000000..9e17975b --- /dev/null +++ b/common/src/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Error: {0}")] + Generic(String), + #[error("IO error: {0}")] + IOError(String), +} diff --git a/common/src/ether/bytecode.rs b/common/src/ether/bytecode.rs index e577d018..f7aa7253 100644 --- a/common/src/ether/bytecode.rs +++ b/common/src/ether/bytecode.rs @@ -2,20 +2,26 @@ use super::rpc::get_code; use crate::{ constants::{ADDRESS_REGEX, BYTECODE_REGEX}, debug_max, + error::Error, utils::io::logging::Logger, }; use std::fs; -pub async fn get_bytecode_from_target( - target: &str, - rpc_url: &str, -) -> Result> { +pub async fn get_bytecode_from_target(target: &str, rpc_url: &str) -> Result { let (logger, _) = Logger::new(""); - if ADDRESS_REGEX.is_match(target)? { + if ADDRESS_REGEX + .is_match(target) + .map_err(|e| Error::Generic(format!("failed to match address regex: {}", e)))? + { // Target is a contract address, so we need to fetch the bytecode from the RPC provider. - get_code(target, rpc_url).await - } else if BYTECODE_REGEX.is_match(target)? { + get_code(target, rpc_url).await.map_err(|e| { + Error::Generic(format!("failed to fetch bytecode from RPC provider: {}", e)) + }) + } else if BYTECODE_REGEX + .is_match(target) + .map_err(|e| Error::Generic(format!("failed to match bytecode regex: {}", e)))? + { debug_max!("using provided bytecode for snapshotting."); // Target is already a bytecode, so we just need to remove 0x from the begining @@ -27,7 +33,11 @@ pub async fn get_bytecode_from_target( match fs::read_to_string(target) { Ok(contents) => { let _contents = contents.replace('\n', ""); - if BYTECODE_REGEX.is_match(&_contents)? && _contents.len() % 2 == 0 { + if BYTECODE_REGEX + .is_match(&_contents) + .map_err(|e| Error::Generic(format!("failed to match bytecode regex: {}", e)))? && + _contents.len() % 2 == 0 + { Ok(_contents.replacen("0x", "", 1)) } else { logger.error(&format!("file '{}' doesn't contain valid bytecode.", &target)); diff --git a/common/src/ether/rpc.rs b/common/src/ether/rpc.rs index b6e40a7f..4ae37a8c 100644 --- a/common/src/ether/rpc.rs +++ b/common/src/ether/rpc.rs @@ -1,4 +1,4 @@ -use crate::{debug_max, utils::io::logging::Logger}; +use crate::{debug_max, error::Error, utils::io::logging::Logger}; use backoff::ExponentialBackoff; use ethers::{ core::types::Address, @@ -16,10 +16,10 @@ use std::{str::FromStr, time::Duration}; /// ```no_run /// use heimdall_common::ether::rpc::chain_id; /// -/// // let chain_id = chain_id("https://eth.llamarpc.com").await.unwrap(); +/// // let chain_id = chain_id("https://eth.llamarpc.com").await?; /// //assert_eq!(chain_id, 1); /// ``` -pub async fn chain_id(rpc_url: &str) -> Result> { +pub async fn chain_id(rpc_url: &str) -> Result { backoff::future::retry( ExponentialBackoff { max_elapsed_time: Some(Duration::from_secs(10)), @@ -33,7 +33,9 @@ pub async fn chain_id(rpc_url: &str) -> Result> // check the cache for a matching rpc url let cache_key = format!("chain_id.{}", &rpc_url.replace('/', "").replace(['.', ':'], "-")); - if let Some(chain_id) = read_cache(&cache_key) { + if let Some(chain_id) = read_cache(&cache_key) + .map_err(|_| logger.error(&format!("failed to read cache for rpc url: {:?}", &rpc_url)))? + { logger.debug(&format!("found cached chain id for rpc url: {:?}", &rpc_url)); return Ok(chain_id) } @@ -63,15 +65,15 @@ pub async fn chain_id(rpc_url: &str) -> Result> }; // cache the results - let _ = store_cache(&cache_key, chain_id.as_u64(), None) - .map_err(|_| logger.error(&format!("failed to cache chain id for rpc url: {:?}", &rpc_url))); + store_cache(&cache_key, chain_id.as_u64(), None) + .map_err(|_| logger.error(&format!("failed to cache chain id for rpc url: {:?}", &rpc_url)))?; debug_max!(&format!("chain_id is '{}'", &chain_id)); Ok(chain_id.as_u64()) }) .await - .map_err(|_| Box::from("failed to fetch chain id")) + .map_err(|e| Error::Generic(format!("failed to get chain id: {:?}", e))) } /// Get the bytecode of the provided contract address @@ -82,10 +84,7 @@ pub async fn chain_id(rpc_url: &str) -> Result> /// // let bytecode = get_code("0x0", "https://eth.llamarpc.com").await; /// // assert!(bytecode.is_ok()); /// ``` -pub async fn get_code( - contract_address: &str, - rpc_url: &str, -) -> Result> { +pub async fn get_code(contract_address: &str, rpc_url: &str) -> Result { backoff::future::retry( ExponentialBackoff { max_elapsed_time: Some(Duration::from_secs(10)), @@ -99,7 +98,9 @@ pub async fn get_code( let chain_id = chain_id(rpc_url).await.unwrap_or(1); // check the cache for a matching address - if let Some(bytecode) = read_cache(&format!("contract.{}.{}", &chain_id, &contract_address)) { + if let Some(bytecode) = read_cache(&format!("contract.{}.{}", &chain_id, &contract_address)) + .map_err(|_| logger.error(&format!("failed to read cache for contract: {:?}", &contract_address)))? + { logger.debug(&format!("found cached bytecode for '{}' .", &contract_address)); return Ok(bytecode) } @@ -140,17 +141,17 @@ pub async fn get_code( }; // cache the results - let _ = store_cache( + store_cache( &format!("contract.{}.{}", &chain_id, &contract_address), bytecode_as_bytes.to_string().replacen("0x", "", 1), None, ) - .map_err(|_| logger.error(&format!("failed to cache bytecode for contract: {:?}", &contract_address))); + .map_err(|_| logger.error(&format!("failed to cache bytecode for contract: {:?}", &contract_address)))?; Ok(bytecode_as_bytes.to_string().replacen("0x", "", 1)) }) .await - .map_err(|_| Box::from("failed to fetch bytecode")) + .map_err(|_| Error::Generic(format!("failed to get bytecode for contract: {:?}", &contract_address))) } /// Get the raw transaction data of the provided transaction hash @@ -162,10 +163,7 @@ pub async fn get_code( /// // assert!(bytecode.is_ok()); /// ``` /// TODO: check for caching -pub async fn get_transaction( - transaction_hash: &str, - rpc_url: &str, -) -> Result> { +pub async fn get_transaction(transaction_hash: &str, rpc_url: &str) -> Result { backoff::future::retry( ExponentialBackoff { max_elapsed_time: Some(Duration::from_secs(10)), @@ -222,7 +220,7 @@ pub async fn get_transaction( Ok(tx) }) .await - .map_err(|_| Box::from("failed to get transaction")) + .map_err(|_| Error::Generic(format!("failed to get transaction: {:?}", &transaction_hash))) } /// Get the storage diff of the provided transaction hash @@ -236,7 +234,7 @@ pub async fn get_transaction( pub async fn get_storage_diff( transaction_hash: &str, rpc_url: &str, -) -> Result, Box> { +) -> Result, Error> { backoff::future::retry( ExponentialBackoff { max_elapsed_time: Some(Duration::from_secs(10)), @@ -247,11 +245,13 @@ pub async fn get_storage_diff( let logger = Logger::default(); // get chain_id - let chain_id = chain_id(rpc_url).await.unwrap(); + let chain_id = chain_id(rpc_url).await + .map_err(|_| logger.error(&format!("failed to get chain id for rpc url: {:?}", &rpc_url)))?; // check the cache for a matching address if let Some(state_diff) = read_cache(&format!("diff.{}.{}", &chain_id, &transaction_hash)) + .map_err(|_| logger.error(&format!("failed to read cache for transaction: {:?}", &transaction_hash)))? { debug_max!("found cached state diff for transaction '{}' .", &transaction_hash); return Ok(state_diff) @@ -284,23 +284,19 @@ pub async fn get_storage_diff( }; // fetch the state diff for the transaction - let state_diff = match provider + let state_diff = provider .trace_replay_transaction(transaction_hash_hex, vec![TraceType::StateDiff]) .await - { - Ok(traces) => traces.state_diff, - Err(e) => { + .map_err(|_| { logger.error(&format!( - "failed to replay and trace transaction '{}' . does your RPC provider support it?", - &transaction_hash - )); - logger.error(&format!("error: '{e}' .")); - return Err(backoff::Error::Permanent(())) - } - }; + "failed to replay and trace transaction '{}' . does your RPC provider support it?", + &transaction_hash + )) + })? + .state_diff; // write the state diff to the cache - let _ = store_cache( + store_cache( &format!("diff.{}.{}", &chain_id, &transaction_hash), &state_diff, None, @@ -310,7 +306,7 @@ pub async fn get_storage_diff( "failed to cache state diff for transaction: {:?}", &transaction_hash )) - }); + })?; debug_max!("fetched state diff for transaction '{}' .", &transaction_hash); @@ -318,7 +314,7 @@ pub async fn get_storage_diff( }, ) .await - .map_err(|_| Box::from("failed to get storage diff")) + .map_err(|_| Error::Generic(format!("failed to get storage diff for transaction: {:?}", &transaction_hash))) } /// Get the raw trace data of the provided transaction hash @@ -330,10 +326,7 @@ pub async fn get_storage_diff( /// // assert!(trace.is_ok()); /// ``` /// TODO: check for caching -pub async fn get_trace( - transaction_hash: &str, - rpc_url: &str, -) -> Result> { +pub async fn get_trace(transaction_hash: &str, rpc_url: &str) -> Result { backoff::future::retry( ExponentialBackoff { max_elapsed_time: Some(Duration::from_secs(10)), @@ -394,7 +387,7 @@ pub async fn get_trace( }, ) .await - .map_err(|_| Box::from("failed to get trace")) + .map_err(|_| Error::Generic(format!("failed to get trace for transaction: {:?}", &transaction_hash))) } /// Get all logs for the given block number @@ -408,7 +401,7 @@ pub async fn get_trace( pub async fn get_block_logs( block_number: u64, rpc_url: &str, -) -> Result, Box> { +) -> Result, Error> { backoff::future::retry( ExponentialBackoff { max_elapsed_time: Some(Duration::from_secs(10)), @@ -430,7 +423,7 @@ pub async fn get_block_logs( }; // fetch the logs for the block - let logs = match provider + let logs = provider .get_logs(&Filter { block_option: FilterBlockOption::Range { from_block: Some(BlockNumber::from(block_number)), @@ -440,17 +433,12 @@ pub async fn get_block_logs( topics: [None, None, None, None], }) .await - { - Ok(logs) => logs, - Err(e) => { + .map_err(|_| { logger.error(&format!( "failed to fetch logs for block '{}' . does your RPC provider support it?", &block_number - )); - logger.error(&format!("error: '{e}' .")); - return Err(backoff::Error::Permanent(())) - } - }; + )) + })?; debug_max!("fetched logs for block '{}' .", &block_number); @@ -458,7 +446,7 @@ pub async fn get_block_logs( }, ) .await - .map_err(|_| Box::from("failed to get logs")) + .map_err(|_| Error::Generic(format!("failed to get logs for block: {:?}", &block_number))) } // TODO: add tests diff --git a/common/src/ether/selectors.rs b/common/src/ether/selectors.rs index e4b67631..6ce11fdd 100644 --- a/common/src/ether/selectors.rs +++ b/common/src/ether/selectors.rs @@ -171,7 +171,7 @@ where // create a new thread for each selector threads.push(task::spawn(async move { - if let Some(function) = T::resolve(&selector).await { + if let Ok(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 2e86374f..61688403 100644 --- a/common/src/ether/signatures.rs +++ b/common/src/ether/signatures.rs @@ -4,6 +4,7 @@ use heimdall_cache::{read_cache, store_cache}; use crate::{ debug_max, + error::Error, utils::{http::get_json_from_url, strings::replace_last}, }; use serde::{Deserialize, Serialize}; @@ -32,14 +33,14 @@ pub struct ResolvedLog { #[async_trait] pub trait ResolveSelector { - async fn resolve(selector: &str) -> Option> + async fn resolve(selector: &str) -> Result>, Error> where Self: Sized; } #[async_trait] impl ResolveSelector for ResolvedError { - async fn resolve(selector: &str) -> Option> { + async fn resolve(selector: &str) -> Result>, Error> { // normalize selector let selector = match selector.strip_prefix("0x") { Some(selector) => selector, @@ -51,12 +52,13 @@ impl ResolveSelector for ResolvedError { // get cached results if let Some(cached_results) = read_cache::>(&format!("selector.{selector}")) + .map_err(|e| Error::Generic(format!("error reading cache: {}", e)))? { match cached_results.len() { - 0 => return None, + 0 => return Ok(None), _ => { debug_max!("found cached results for selector: {}", &selector); - return Some(cached_results) + return Ok(Some(cached_results)) } } } @@ -70,19 +72,20 @@ impl ResolveSelector for ResolvedError { 10, ) .await - .unwrap() + .map_err(|e| Error::Generic(format!("error fetching signatures from openchain: {}", e)))? { Some(signatures) => signatures, - None => return None, + None => return Ok(None), }; // convert the serde value into a vec of possible functions let results = signatures - .get("result")? - .get("function")? - .get(&format!("0x{selector}"))? - .as_array()? - .to_vec(); + .get("result") + .and_then(|result| result.get("function")) + .and_then(|function| function.get(format!("0x{selector}"))) + .and_then(|item| item.as_array()) + .map(|array| array.to_vec()) + .ok_or_else(|| Error::Generic("error parsing signatures from openchain".to_string()))?; debug_max!("found {} possible functions for selector: {}", &results.len(), &selector); @@ -115,16 +118,16 @@ impl ResolveSelector for ResolvedError { let _ = store_cache(&format!("selector.{selector}"), signature_list.clone(), None) .map_err(|e| debug_max!("error storing signatures in cache: {}", e)); - match signature_list.len() { + Ok(match signature_list.len() { 0 => None, _ => Some(signature_list), - } + }) } } #[async_trait] impl ResolveSelector for ResolvedLog { - async fn resolve(selector: &str) -> Option> { + async fn resolve(selector: &str) -> Result>, Error> { // normalize selector let selector = match selector.strip_prefix("0x") { Some(selector) => selector, @@ -136,12 +139,13 @@ impl ResolveSelector for ResolvedLog { // get cached results if let Some(cached_results) = read_cache::>(&format!("selector.{selector}")) + .map_err(|e| Error::Generic(format!("error reading cache: {}", e)))? { match cached_results.len() { - 0 => return None, + 0 => return Ok(None), _ => { debug_max!("found cached results for selector: {}", &selector); - return Some(cached_results) + return Ok(Some(cached_results)) } } } @@ -155,19 +159,20 @@ impl ResolveSelector for ResolvedLog { 10, ) .await - .unwrap() + .map_err(|e| Error::Generic(format!("error fetching signatures from openchain: {}", e)))? { Some(signatures) => signatures, - None => return None, + None => return Ok(None), }; // convert the serde value into a vec of possible functions let results = signatures - .get("result")? - .get("event")? - .get(&format!("0x{selector}"))? - .as_array()? - .to_vec(); + .get("result") + .and_then(|result| result.get("event")) + .and_then(|function| function.get(format!("0x{selector}"))) + .and_then(|item| item.as_array()) + .map(|array| array.to_vec()) + .ok_or_else(|| Error::Generic("error parsing signatures from openchain".to_string()))?; debug_max!("found {} possible functions for selector: {}", &results.len(), &selector); @@ -200,16 +205,16 @@ impl ResolveSelector for ResolvedLog { let _ = store_cache(&format!("selector.{selector}"), signature_list.clone(), None) .map_err(|e| debug_max!("error storing signatures in cache: {}", e)); - match signature_list.len() { + Ok(match signature_list.len() { 0 => None, _ => Some(signature_list), - } + }) } } #[async_trait] impl ResolveSelector for ResolvedFunction { - async fn resolve(selector: &str) -> Option> { + async fn resolve(selector: &str) -> Result>, Error> { // normalize selector let selector = match selector.strip_prefix("0x") { Some(selector) => selector, @@ -221,12 +226,13 @@ impl ResolveSelector for ResolvedFunction { // get cached results if let Some(cached_results) = read_cache::>(&format!("selector.{selector}")) + .map_err(|e| Error::Generic(format!("error reading cache: {}", e)))? { match cached_results.len() { - 0 => return None, + 0 => return Ok(None), _ => { debug_max!("found cached results for selector: {}", &selector); - return Some(cached_results) + return Ok(Some(cached_results)) } } } @@ -240,19 +246,20 @@ impl ResolveSelector for ResolvedFunction { 10, ) .await - .unwrap() + .map_err(|e| Error::Generic(format!("error fetching signatures from openchain: {}", e)))? { Some(signatures) => signatures, - None => return None, + None => return Ok(None), }; // convert the serde value into a vec of possible functions let results = signatures - .get("result")? - .get("function")? - .get(&format!("0x{selector}"))? - .as_array()? - .to_vec(); + .get("result") + .and_then(|result| result.get("function")) + .and_then(|function| function.get(format!("0x{selector}"))) + .and_then(|item| item.as_array()) + .map(|array| array.to_vec()) + .ok_or_else(|| Error::Generic("error parsing signatures from openchain".to_string()))?; debug_max!("found {} possible functions for selector: {}", &results.len(), &selector); @@ -286,10 +293,10 @@ impl ResolveSelector for ResolvedFunction { let _ = store_cache(&format!("selector.{selector}"), signature_list.clone(), None) .map_err(|e| debug_max!("error storing signatures in cache: {}", e)); - match signature_list.len() { + Ok(match signature_list.len() { 0 => None, _ => Some(signature_list), - } + }) } } @@ -318,8 +325,8 @@ mod tests { #[tokio::test] async fn resolve_function_signature_nominal() { let signature = String::from("095ea7b3"); - delete_cache(&format!("selector.{}", &signature)); - let result = ResolvedFunction::resolve(&signature).await; + let _ = delete_cache(&format!("selector.{}", &signature)); + let result = ResolvedFunction::resolve(&signature).await.unwrap(); assert!(result.is_some()); assert!(!result.unwrap().is_empty()); } @@ -327,8 +334,8 @@ mod tests { #[tokio::test] async fn resolve_error_signature_nominal() { let signature = String::from("30cd7471"); - delete_cache(&format!("selector.{}", &signature)); - let result = ResolvedError::resolve(&signature).await; + let _ = delete_cache(&format!("selector.{}", &signature)); + let result = ResolvedError::resolve(&signature).await.unwrap(); assert!(result.is_some()); assert!(!result.unwrap().is_empty()); } @@ -337,8 +344,8 @@ mod tests { async fn resolve_event_signature_nominal() { let signature = String::from("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); - delete_cache(&format!("selector.{}", &signature)); - let result = ResolvedLog::resolve(&signature).await; + let _ = delete_cache(&format!("selector.{}", &signature)); + let result = ResolvedLog::resolve(&signature).await.unwrap(); assert!(result.is_some()); assert!(!result.unwrap().is_empty()); } @@ -346,7 +353,7 @@ mod tests { #[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).await; + let result = ResolvedFunction::resolve(&signature).await.unwrap(); assert_eq!(result, None,) } @@ -354,51 +361,51 @@ mod tests { #[tokio::test] async fn resolve_function_signature_should_return_none_when_json_url_returns_empty_signatures() { - delete_cache(&format!("selector.{}", "test_signature")); + let _ = delete_cache(&format!("selector.{}", "test_signature")); let signature = String::from("test_signature"); - let result = ResolvedFunction::resolve(&signature).await; + let result = ResolvedFunction::resolve(&signature).await.unwrap(); assert_eq!(result, None); } #[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).await; + let result = ResolvedError::resolve(&signature).await.unwrap(); assert_eq!(result, 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).await; + let result = ResolvedError::resolve(&signature).await.unwrap(); assert_eq!(result, None); } #[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).await; + let result = ResolvedError::resolve(&signature).await.unwrap(); assert_eq!(result, None); } #[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).await; + let result = ResolvedLog::resolve(&signature).await.unwrap(); assert_eq!(result, 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).await; + let result = ResolvedLog::resolve(&signature).await.unwrap(); assert_eq!(result, None); } #[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).await; + let result = ResolvedLog::resolve(&signature).await.unwrap(); assert_eq!(result, None); } diff --git a/common/src/lib.rs b/common/src/lib.rs index 5bab5300..705e12a2 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,6 +1,7 @@ extern crate lazy_static; pub mod constants; +pub mod error; pub mod ether; pub mod resources; pub mod utils; diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 96dbca74..b7beb6bf 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -110,7 +110,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { logger.error("OpenAI API key is required for explaining calldata. Use `heimdall decode --help` for more information."); return Err(Error::GenericError( "OpenAI API key is required for explaining calldata.".to_string(), - )) + )); } // init variables @@ -118,7 +118,10 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { let mut calldata; // determine whether or not the target is a transaction hash - if TRANSACTION_HASH_REGEX.is_match(&args.target).unwrap() { + if TRANSACTION_HASH_REGEX + .is_match(&args.target) + .map_err(|_| Error::GenericError("failed to match transaction hash regex.".to_string()))? + { // We are decoding a transaction hash, so we need to fetch the calldata from the RPC // provider. raw_transaction = get_transaction(&args.target, &args.rpc_url).await.map_err(|_| { @@ -126,20 +129,23 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { })?; calldata = raw_transaction.input.to_string().replacen("0x", "", 1); - } else if CALLDATA_REGEX.is_match(&args.target).unwrap() { + } else if CALLDATA_REGEX + .is_match(&args.target) + .map_err(|_| Error::GenericError("failed to match calldata regex.".to_string()))? + { // We are decoding raw calldata, so we can just use the provided calldata. calldata = args.target.to_string().replacen("0x", "", 1); } else { logger.error("invalid target. must be a transaction hash or calldata (bytes)."); return Err(Error::GenericError( "invalid target. must be a transaction hash or calldata (bytes).".to_string(), - )) + )); } // check if the calldata length is a standard length if calldata.len() % 2 != 0 || calldata.len() < 8 { logger.error("calldata is not a valid hex string."); - return Err(Error::GenericError("calldata is not a valid hex string.".to_string())) + return Err(Error::GenericError("calldata is not a valid hex string.".to_string())); } // if calldata isn't a multiple of 64, it may be harder to decode. @@ -161,13 +167,15 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { Ok(byte_args) => byte_args, Err(_) => { logger.error("failed to parse bytearray from calldata."); - return Err(Error::DecodeError) + return Err(Error::DecodeError); } }; // get the function signature possibilities let potential_matches = if !args.skip_resolving { - match ResolvedFunction::resolve(&function_selector).await { + match ResolvedFunction::resolve(&function_selector).await.map_err(|e| { + Error::GenericError(format!("failed to resolve function selector: {}", e)) + })? { Some(signatures) => signatures, None => Vec::new(), } @@ -219,7 +227,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { 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 + continue; } }; @@ -276,10 +284,10 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { .as_bytes() .chunks(64) .map(|chunk| { - let s = std::str::from_utf8(chunk).unwrap(); + let s = std::str::from_utf8(chunk).map_err(|_| Error::DecodeError); s }) - .collect::>(); + .collect::, Error>>()?; // while calldata_words is not empty, iterate over it let mut i = 0; @@ -337,7 +345,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { matches.push(resolved_function); } else { logger.error("failed to dynamically decode calldata."); - return Err(Error::DecodeError) + return Err(Error::DecodeError); } } @@ -364,7 +372,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { Some(selected_match) => selected_match, None => { logger.error("invalid selection."); - return Err(Error::GenericError("invalid selection.".to_string())) + return Err(Error::GenericError("invalid selection.".to_string())); } }; @@ -401,10 +409,12 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { ); // build inputs - for (i, input) in selected_match.decoded_inputs.as_ref().unwrap().iter().enumerate() { + for (i, input) in + selected_match.decoded_inputs.as_ref().ok_or(Error::DecodeError)?.iter().enumerate() + { let mut decoded_inputs_as_message = display(vec![input.to_owned()], " "); if decoded_inputs_as_message.is_empty() { - break + break; } if i == 0 { @@ -468,7 +478,7 @@ fn try_decode(inputs: &[ParamType], byte_args: &[u8]) -> Result<(Vec, Vec }); } - return Ok((result, params)) + return Ok((result, params)); } Err(Error::DecodeError) diff --git a/core/src/inspect/core/logs.rs b/core/src/inspect/core/logs.rs index 93a52808..013e91f3 100644 --- a/core/src/inspect/core/logs.rs +++ b/core/src/inspect/core/logs.rs @@ -92,7 +92,12 @@ impl TryFrom for DecodedLog { resolved_logs = match signature { Some(signature) => { debug_max!("resolving signature: {}", signature.to_string().to_lowercase()); - ResolvedLog::resolve(&signature).await.unwrap_or(Vec::new()) + ResolvedLog::resolve(&signature) + .await + .map_err(|e| { + Self::Error::GenericError(format!("failed to resolve signature: {}", e)) + })? + .unwrap_or(Vec::new()) } None => Vec::new(), };