diff --git a/main/src/main.rs b/main/src/main.rs index 60baf75..82e781a 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -4,7 +4,8 @@ use alloy_primitives::TxHash; use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand}; use constants::DEFAULT_ENDPOINT; -use ethers::types::H160; +use ethers::abi::Bytes; +use ethers::types::{H160, U256}; use eyre::{bail, eyre, Context, Result}; use std::path::PathBuf; use std::{fmt, path::Path}; @@ -97,6 +98,9 @@ enum Apis { /// Trace a transaction. #[command(visible_alias = "t")] Trace(TraceArgs), + /// Simulate a transaction. + #[command(visible_alias = "s")] + Simulate(SimulateArgs), } #[derive(Args, Clone, Debug)] @@ -266,6 +270,45 @@ struct TraceArgs { use_native_tracer: bool, } +#[derive(Args, Clone, Debug)] +pub struct SimulateArgs { + /// RPC endpoint. + #[arg(short, long, default_value = "http://localhost:8547")] + endpoint: String, + + /// From address. + #[arg(short, long)] + from: Option, + + /// To address. + #[arg(short, long)] + to: Option, + + /// Gas limit. + #[arg(long)] + gas: Option, + + /// Gas price. + #[arg(long)] + gas_price: Option, + + /// Value to send with the transaction. + #[arg(short, long)] + value: Option, + + /// Data to send with the transaction, as a hex string (with or without '0x' prefix). + #[arg(short, long)] + data: Option, + + /// Project path. + #[arg(short, long, default_value = ".")] + project: PathBuf, + + /// If set, use the native tracer instead of the JavaScript one. + #[arg(short, long, default_value_t = false)] + use_native_tracer: bool, +} + #[derive(Clone, Debug, Args)] #[clap(group(ArgGroup::new("key").required(true).args(&["private_key_path", "private_key", "keystore_path"])))] struct AuthOpts { @@ -473,6 +516,9 @@ async fn main_impl(args: Opts) -> Result<()> { "stylus activate failed" ); } + Apis::Simulate(args) => { + run!(simulate(args).await, "failed to simulate transaction"); + } Apis::Cgen { input, out_dir } => { run!(gen::c_gen(&input, &out_dir), "failed to generate c code"); } @@ -557,6 +603,13 @@ async fn trace(args: TraceArgs) -> Result<()> { Ok(()) } +async fn simulate(args: SimulateArgs) -> Result<()> { + let provider = sys::new_provider(&args.endpoint)?; + let trace = Trace::simulate(provider, &args).await?; + println!("{}", trace.json); + Ok(()) +} + async fn replay(args: ReplayArgs) -> Result<()> { if !args.child { let rust_gdb = sys::command_exists("rust-gdb"); diff --git a/main/src/trace.rs b/main/src/trace.rs index 207f353..2253855 100644 --- a/main/src/trace.rs +++ b/main/src/trace.rs @@ -4,10 +4,14 @@ #![allow(clippy::redundant_closure_call)] use crate::util::color::{Color, DebugColor}; +use crate::SimulateArgs; use alloy_primitives::{Address, TxHash, B256, U256}; use ethers::{ providers::{JsonRpcClient, Middleware, Provider}, - types::{GethDebugTracerType, GethDebugTracingOptions, GethTrace, Transaction}, + types::{ + BlockId, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, + GethTrace, Transaction, TransactionRequest, + }, utils::__serde_json::{from_value, Value}, }; use eyre::{bail, OptionExt, Result, WrapErr}; @@ -77,6 +81,91 @@ impl Trace { frame: self.top_frame, } } + pub async fn simulate( + provider: Provider, + args: &SimulateArgs, + ) -> Result { + // Build the transaction request + let mut tx_request = TransactionRequest::new(); + + if let Some(from) = args.from { + tx_request = tx_request.from(from); + } + if let Some(to) = args.to { + tx_request = tx_request.to(to); + } + if let Some(gas) = args.gas { + tx_request = tx_request.gas(gas); + } + if let Some(gas_price) = args.gas_price { + tx_request = tx_request.gas_price(gas_price); + } + if let Some(value) = args.value { + tx_request = tx_request.value(value); + } + if let Some(data) = &args.data { + tx_request = tx_request.data(data.clone()); + } + + // Use the same tracer as in Trace::new + let query = if args.use_native_tracer { + "stylusTracer" + } else { + include_str!("query.js") + }; + + // Corrected construction of tracer_options + let tracer_options = GethDebugTracingCallOptions { + tracing_options: GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::JsTracer(query.to_owned())), + ..Default::default() + }, + ..Default::default() + }; + + // Use the latest block; alternatively, this can be made configurable + let block_id = None::; + + let GethTrace::Unknown(json) = provider + .debug_trace_call(tx_request, block_id, tracer_options) + .await? + else { + bail!("Malformed tracing result"); + }; + + if let Value::Array(arr) = json.clone() { + if arr.is_empty() { + bail!("No trace frames found."); + } + } + // Since this is a simulated transaction, we create a dummy Transaction object + let tx = Transaction { + from: args.from.unwrap_or_default(), + to: args.to, + gas: args + .gas + .map(|gas| { + let bytes = [0u8; 32]; // U256 in both libraries is 32 bytes + gas.to_be_bytes().copy_from_slice(&bytes[..8]); // Convert alloy_primitives::U256 to bytes + ethers::types::U256::from_big_endian(&bytes) // Convert bytes to ethers::types::U256 + }) + .unwrap_or_else(ethers::types::U256::zero), // Default to 0 if no gas is provided + gas_price: args.gas_price, + value: args.value.unwrap_or_else(ethers::types::U256::zero), + input: args.data.clone().unwrap_or_default().into(), + // Default values for other fields + ..Default::default() + }; + + // Parse the trace frames + let top_frame = TraceFrame::parse_frame(None, json.clone())?; + + Ok(Self { + top_frame, + tx, + json, + }) + } } #[derive(Serialize, Deserialize)]