diff --git a/.gitignore b/.gitignore index 997bc1e3..56abf6e3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ false/* *.sh largest1k + +bun.lockb +node_modules diff --git a/Cargo.lock b/Cargo.lock index 281ac60e..2735ad0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2305,6 +2305,7 @@ dependencies = [ "heimdall-common", "heimdall-config", "heimdall-vm", + "serde", "serde_json", "thiserror 1.0.69", "tracing", diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c562322a..a747b4d6 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -160,9 +160,18 @@ async fn main() -> Result<()> { } let result = - decode(cmd).await.map_err(|e| eyre!("failed to decode calldata: {}", e))?; + decode(cmd.clone()).await.map_err(|e| eyre!("failed to decode calldata: {}", e))?; - result.display() + if cmd.output == "print" { + result.display() + } else { + let output_path = + build_output_path(&cmd.output, &cmd.target, &cmd.rpc_url, "decoded.json") + .await + .map_err(|e| eyre!("failed to build output path: {}", e))?; + write_file(&output_path, &result.decoded.to_json()?) + .map_err(|e| eyre!("failed to write decoded output: {}", e))?; + } } Subcommands::Cfg(mut cmd) => { diff --git a/crates/common/src/ether/signatures.rs b/crates/common/src/ether/signatures.rs index 8bc507fa..51234db8 100644 --- a/crates/common/src/ether/signatures.rs +++ b/crates/common/src/ether/signatures.rs @@ -17,6 +17,8 @@ use heimdall_cache::{store_cache, with_cache}; use serde::{Deserialize, Serialize}; use tracing::{debug, trace}; +use super::types::DynSolValueExt; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ResolvedFunction { pub name: String, @@ -30,6 +32,31 @@ impl ResolvedFunction { pub fn inputs(&self) -> Vec { parse_function_parameters(&self.signature).expect("invalid signature") } + + /// A helper function to convert the struct into a JSON string. + /// We use this because `decoded_inputs` cannot be serialized by serde. + pub fn to_json(&self) -> Result { + Ok(format!( + r#"{{ + "name": "{}", + "signature": "{}", + "inputs": {}, + "decoded_inputs": [{}] +}}"#, + &self.name, + &self.signature, + serde_json::to_string(&self.inputs)?, + if let Some(decoded_inputs) = &self.decoded_inputs { + decoded_inputs + .iter() + .map(|input| input.serialize().to_string()) + .collect::>() + .join(", ") + } else { + "".to_string() + } + )) + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/crates/core/tests/test_decode.rs b/crates/core/tests/test_decode.rs index c0ca0b6f..dce0030a 100644 --- a/crates/core/tests/test_decode.rs +++ b/crates/core/tests/test_decode.rs @@ -16,6 +16,7 @@ mod integration_tests { truncate_calldata: false, skip_resolving: false, raw: false, + output: String::from("print"), }; let _ = heimdall_decoder::decode(args).await; } @@ -33,6 +34,8 @@ mod integration_tests { truncate_calldata: false, skip_resolving: false, raw: false, + output: String::from("print"), + }; let _ = heimdall_decoder::decode(args).await; } diff --git a/crates/decode/Cargo.toml b/crates/decode/Cargo.toml index cfba4936..3a39b398 100644 --- a/crates/decode/Cargo.toml +++ b/crates/decode/Cargo.toml @@ -25,6 +25,11 @@ eyre = "0.6.12" heimdall-vm.workspace = true alloy-dyn-abi = "0.8.3" alloy-json-abi = "0.8.3" -alloy = { version = "0.3.3", features = ["full", "rpc-types-debug", "rpc-types-trace"] } +alloy = { version = "0.3.3", features = [ + "full", + "rpc-types-debug", + "rpc-types-trace", +] } serde_json = "1.0" hashbrown = "0.14.5" +serde = "1.0" diff --git a/crates/decode/src/interfaces/args.rs b/crates/decode/src/interfaces/args.rs index 32f3b408..7ef05ee0 100644 --- a/crates/decode/src/interfaces/args.rs +++ b/crates/decode/src/interfaces/args.rs @@ -21,7 +21,7 @@ pub struct DecodeArgs { pub rpc_url: String, /// Your OpenAI API key, used for explaining calldata. - #[clap(long, short, default_value = "", hide_default_value = true)] + #[clap(long, default_value = "", hide_default_value = true)] pub openai_api_key: String, /// Whether to explain the decoded calldata using OpenAI. @@ -46,12 +46,16 @@ pub struct DecodeArgs { /// Whether to treat the target as a raw calldata string. Useful if the target is exactly 32 /// bytes. - #[clap(long, short)] + #[clap(long)] pub raw: bool, /// Path to an optional ABI file to use for resolving errors, functions, and events. #[clap(long, short, default_value = None, hide_default_value = true)] pub abi: Option, + + /// The output directory to write the output to or 'print' to print to the console + #[clap(long = "output", short = 'o', default_value = "print", hide_default_value = true)] + pub output: String, } impl DecodeArgs { @@ -73,6 +77,7 @@ impl DecodeArgsBuilder { skip_resolving: Some(false), raw: Some(false), abi: Some(None), + output: Some(String::from("print")), } } } diff --git a/examples/typescript/README.md b/examples/typescript/README.md new file mode 100644 index 00000000..4caab6d0 --- /dev/null +++ b/examples/typescript/README.md @@ -0,0 +1,9 @@ +# Example: Invoking Heimdall via TypeScript + +This TypeScript script demonstrates how to use the `heimdall` CLI tool to decode calldata via TypeScript. It provides a simple class structure to define arguments and manage the decode process, with support for customizing various decode options. + +_Note: This is just an example for the decode module, but a similar approach will work for all heimdall modules._ + +## Overview + +The script utilizes the `heimdall decode` command to decode a target. For ease of use, the script abstracts the command-line interface of `heimdall` into a TS class, allowing users to easily call the decode process in their TS projects. diff --git a/examples/typescript/index.ts b/examples/typescript/index.ts new file mode 100644 index 00000000..6ece9016 --- /dev/null +++ b/examples/typescript/index.ts @@ -0,0 +1,103 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +interface DecodeArgsOptions { + target: string; + rpc_url?: string; + default?: boolean; + skip_resolving?: boolean; + raw?: boolean; +} + +class DecodeArgs { + public target: string; + public rpc_url: string; + public default: boolean; + public skip_resolving: boolean; + public raw: boolean; + + constructor( + target: string, + rpc_url: string = "", + useDefault: boolean = false, + skip_resolving: boolean = false, + raw: boolean = false + ) { + this.target = target; + this.rpc_url = rpc_url; + this.default = useDefault; + this.skip_resolving = skip_resolving; + this.raw = raw; + } +} + +class Decoder { + private args: DecodeArgs; + + constructor(args: DecodeArgs) { + this.args = args; + } + + public decode(): any | null { + try { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'decoder-')); + + const command = ['decode', this.args.target, ]; + + if (this.args.rpc_url) { + command.push('--rpc-url', this.args.rpc_url); + } + if (this.args.default) { + command.push('--default'); + } + if (this.args.skip_resolving) { + command.push('--skip-resolving'); + } + if (this.args.raw) { + command.push('--raw'); + } + + // Execute heimdall command + execSync(`heimdall ${command.join(' ')}`, { stdio: 'inherit' }); + + // Here you would read and parse the output from `tempDir` + // For now, we return null since the original code doesn't show the parsing step. + return null; + } catch (e) { + console.error("Error: ", e); + return null; + } + } +} + +function isHeimdallInstalled(): boolean { + try { + execSync('which heimdall', { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +function main() { + if (!isHeimdallInstalled()) { + console.log("heimdall does not seem to be installed on your system."); + console.log("please install heimdall before running this script."); + return; + } + + const args = new DecodeArgs( + "0x000000000000000000000000008dfede2ef0e61578c3bba84a7ed4b9d25795c30000000000000000000000000000000000000001431e0fae6d7217caa00000000000000000000000000000000000000000000000000000000000000000002710fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc7c0000000000000000000000000000000000000000000000a6af776004abf4e612ad000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000000000111700000000000000000000000001c5f545f5b46f76e440fa02dabf88fdc0b10851a00000000000000000000000000000000000000000000000000000002540be400", + "", + false, + false, + true + ); + + const decoded = new Decoder(args).decode(); + console.log("Decoded Result:", decoded); +} + +main(); diff --git a/examples/typescript/package.json b/examples/typescript/package.json new file mode 100644 index 00000000..af94c856 --- /dev/null +++ b/examples/typescript/package.json @@ -0,0 +1,16 @@ +{ + "name": "heimdall-ts", + "version": "1.0.0", + "main": "dist/index.js", + "type": "module", + "scripts": { + "decode": "tsx index.ts" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "typescript": "^5.5.4" + } +}