diff --git a/Cargo.lock b/Cargo.lock index 5f88c98e14be1..869c98f9f136a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4462,6 +4462,7 @@ dependencies = [ "alloy-rpc-types", "alloy-serde", "chrono", + "eyre", "foundry-macros", "revm", "serde", diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 7d84c0bfcc351..436a845043897 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -16,7 +16,7 @@ use eyre::Result; use foundry_cli::{handler, utils, utils::LoadConfig}; use foundry_common::{ abi::{get_error, get_event}, - fmt::{format_tokens, format_tokens_raw, format_uint_exp}, + fmt::{format_tokens, format_uint_exp, serialize_value_as_json}, fs, selectors::{ ParsedSignatures, SelectorImportData, SelectorKind, decode_calldata, decode_event_topic, @@ -751,13 +751,18 @@ pub async fn run_command(args: CastArgs) -> Result<()> { } }; - /// Prints slice of tokens using [`format_tokens`] or [`format_tokens_raw`] depending whether - /// the shell is in JSON mode. + /// Prints slice of tokens using [`format_tokens`] or [`serialize_value_as_json`] depending + /// whether the shell is in JSON mode. /// /// This is included here to avoid a cyclic dependency between `fmt` and `common`. fn print_tokens(tokens: &[DynSolValue]) { if shell::is_json() { - let tokens: Vec = format_tokens_raw(tokens).collect(); + let tokens: Vec = tokens + .iter() + .cloned() + .map(serialize_value_as_json) + .collect::>>() + .unwrap(); let _ = sh_println!("{}", serde_json::to_string_pretty(&tokens).unwrap()); } else { let tokens = format_tokens(tokens); diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index c081952889a5d..0f046b4f7a94b 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -2313,7 +2313,7 @@ fn explorer_client( #[cfg(test)] mod tests { - use super::SimpleCast as Cast; + use super::{DynSolValue, SimpleCast as Cast, serialize_value_as_json}; use alloy_primitives::hex; #[test] @@ -2431,6 +2431,47 @@ mod tests { ); } + #[test] + fn calldata_decode_nested_json() { + let calldata = "0xdb5b0ed700000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000006772bf190000000000000000000000000000000000000000000000000000000000020716000000000000000000000000af9d27ffe4d51ed54ac8eec78f2785d7e11e5ab100000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000000404366a6dc4b2f348a85e0066e46f0cc206fca6512e0ed7f17ca7afb88e9a4c27000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093922dee6e380c28a50c008ab167b7800bb24c2026cd1b22f1c6fb884ceed7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060f85e59ecad6c1a6be343a945abedb7d5b5bfad7817c4d8cc668da7d391faf700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093dfbf04395fbec1f1aed4ad0f9d3ba880ff58a60485df5d33f8f5e0fb73188600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa334a426ea9e21d5f84eb2d4723ca56b92382b9260ab2b6769b7c23d437b6b512322a25cecc954127e60cf91ef056ac1da25f90b73be81c3ff1872fa48d10c7ef1ccb4087bbeedb54b1417a24abbb76f6cd57010a65bb03c7b6602b1eaf0e32c67c54168232d4edc0bfa1b815b2af2a2d0a5c109d675a4f2de684e51df9abb324ab1b19a81bac80f9ce3a45095f3df3a7cf69ef18fc08e94ac3cbc1c7effeacca68e3bfe5d81e26a659b5"; + let sig = "sequenceBatchesValidium((bytes32,bytes32,uint64,bytes32)[],uint64,uint64,address,bytes)"; + let decoded = Cast::calldata_decode(sig, calldata, true).unwrap(); + let json_value = serialize_value_as_json(DynSolValue::Array(decoded)).unwrap(); + let expected = serde_json::json!([ + [ + [ + "0x04366a6dc4b2f348a85e0066e46f0cc206fca6512e0ed7f17ca7afb88e9a4c27", + "0x0000000000000000000000000000000000000000000000000000000000000000", + 0, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + [ + "0x093922dee6e380c28a50c008ab167b7800bb24c2026cd1b22f1c6fb884ceed74", + "0x0000000000000000000000000000000000000000000000000000000000000000", + 0, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + [ + "0x60f85e59ecad6c1a6be343a945abedb7d5b5bfad7817c4d8cc668da7d391faf7", + "0x0000000000000000000000000000000000000000000000000000000000000000", + 0, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + [ + "0x93dfbf04395fbec1f1aed4ad0f9d3ba880ff58a60485df5d33f8f5e0fb731886", + "0x0000000000000000000000000000000000000000000000000000000000000000", + 0, + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + ], + 1735573273, + 132886, + "0xAF9d27ffe4d51eD54AC8eEc78f2785D7E11E5ab1", + "0x334a426ea9e21d5f84eb2d4723ca56b92382b9260ab2b6769b7c23d437b6b512322a25cecc954127e60cf91ef056ac1da25f90b73be81c3ff1872fa48d10c7ef1ccb4087bbeedb54b1417a24abbb76f6cd57010a65bb03c7b6602b1eaf0e32c67c54168232d4edc0bfa1b815b2af2a2d0a5c109d675a4f2de684e51df9abb324ab1b19a81bac80f9ce3a45095f3df3a7cf69ef18fc08e94ac3cbc1c7effeacca68e3bfe5d81e26a659b5" + ]); + assert_eq!(json_value, expected); + } + #[test] fn concat_hex() { assert_eq!(Cast::concat_hex(["0x00", "0x01"]), "0x0001"); diff --git a/crates/cast/tests/cli/selectors.rs b/crates/cast/tests/cli/selectors.rs index d3a7e84b88079..9cc7e4e61637f 100644 --- a/crates/cast/tests/cli/selectors.rs +++ b/crates/cast/tests/cli/selectors.rs @@ -140,7 +140,7 @@ casttest!(event_decode_with_sig, |_prj, cmd| { cmd.args(["--json"]).assert_success().stdout_eq(str![[r#" [ - "78", + 78, "0x0000000000000000000000000000000000D0004F" ] @@ -168,7 +168,7 @@ casttest!(error_decode_with_sig, |_prj, cmd| { cmd.args(["--json"]).assert_success().stdout_eq(str![[r#" [ - "101", + 101, "0x0000000000000000000000000000000000D0004F" ] diff --git a/crates/cheatcodes/src/json.rs b/crates/cheatcodes/src/json.rs index 932555041d7da..7191a68e68d18 100644 --- a/crates/cheatcodes/src/json.rs +++ b/crates/cheatcodes/src/json.rs @@ -4,7 +4,7 @@ use crate::{Cheatcode, Cheatcodes, Result, Vm::*, string}; use alloy_dyn_abi::{DynSolType, DynSolValue, Resolver, eip712_parser::EncodeType}; use alloy_primitives::{Address, B256, I256, hex}; use alloy_sol_types::SolValue; -use foundry_common::fs; +use foundry_common::{fmt::serialize_value_as_json, fs}; use foundry_config::fs_permissions::FsAccessKind; use serde_json::{Map, Value}; use std::{borrow::Cow, collections::BTreeMap}; @@ -585,49 +585,6 @@ pub(super) fn json_value_to_token(value: &Value) -> Result { } } -/// Serializes given [DynSolValue] into a [serde_json::Value]. -fn serialize_value_as_json(value: DynSolValue) -> Result { - match value { - DynSolValue::Bool(b) => Ok(Value::Bool(b)), - DynSolValue::String(s) => { - // Strings are allowed to contain stringified JSON objects, so we try to parse it like - // one first. - if let Ok(map) = serde_json::from_str(&s) { - Ok(Value::Object(map)) - } else { - Ok(Value::String(s)) - } - } - DynSolValue::Bytes(b) => Ok(Value::String(hex::encode_prefixed(b))), - DynSolValue::FixedBytes(b, size) => Ok(Value::String(hex::encode_prefixed(&b[..size]))), - DynSolValue::Int(i, _) => { - // let serde handle number parsing - let n = serde_json::from_str(&i.to_string())?; - Ok(Value::Number(n)) - } - DynSolValue::Uint(i, _) => { - // let serde handle number parsing - let n = serde_json::from_str(&i.to_string())?; - Ok(Value::Number(n)) - } - DynSolValue::Address(a) => Ok(Value::String(a.to_string())), - DynSolValue::Array(e) | DynSolValue::FixedArray(e) => { - Ok(Value::Array(e.into_iter().map(serialize_value_as_json).collect::>()?)) - } - DynSolValue::CustomStruct { name: _, prop_names, tuple } => { - let values = - tuple.into_iter().map(serialize_value_as_json).collect::>>()?; - let map = prop_names.into_iter().zip(values).collect(); - - Ok(Value::Object(map)) - } - DynSolValue::Tuple(values) => Ok(Value::Array( - values.into_iter().map(serialize_value_as_json).collect::>()?, - )), - DynSolValue::Function(_) => bail!("cannot serialize function pointer"), - } -} - /// Serializes a key:value pair to a specific object. If the key is valueKey, the value is /// expected to be an object, which will be set as the root object for the provided object key, /// overriding the whole root object if the object key already exists. By calling this function diff --git a/crates/common/fmt/Cargo.toml b/crates/common/fmt/Cargo.toml index 2c9c5223df30f..e9ca842748648 100644 --- a/crates/common/fmt/Cargo.toml +++ b/crates/common/fmt/Cargo.toml @@ -16,6 +16,7 @@ workspace = true [dependencies] alloy-primitives.workspace = true alloy-dyn-abi = { workspace = true, features = ["eip712"] } +eyre.workspace = true # ui alloy-consensus.workspace = true diff --git a/crates/common/fmt/src/dynamic.rs b/crates/common/fmt/src/dynamic.rs index 44e8fbd662b95..38be9eab328db 100644 --- a/crates/common/fmt/src/dynamic.rs +++ b/crates/common/fmt/src/dynamic.rs @@ -1,6 +1,8 @@ use super::{format_int_exp, format_uint_exp}; use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_primitives::hex; +use eyre::Result; +use serde_json::Value; use std::fmt; /// [`DynSolValue`] formatter. @@ -146,6 +148,49 @@ pub fn format_token_raw(value: &DynSolValue) -> String { DynValueDisplay::new(value, true).to_string() } +/// Serializes given [DynSolValue] into a [serde_json::Value]. +pub fn serialize_value_as_json(value: DynSolValue) -> Result { + match value { + DynSolValue::Bool(b) => Ok(Value::Bool(b)), + DynSolValue::String(s) => { + // Strings are allowed to contain stringified JSON objects, so we try to parse it like + // one first. + if let Ok(map) = serde_json::from_str(&s) { + Ok(Value::Object(map)) + } else { + Ok(Value::String(s)) + } + } + DynSolValue::Bytes(b) => Ok(Value::String(hex::encode_prefixed(b))), + DynSolValue::FixedBytes(b, size) => Ok(Value::String(hex::encode_prefixed(&b[..size]))), + DynSolValue::Int(i, _) => { + // let serde handle number parsing + let n = serde_json::from_str(&i.to_string())?; + Ok(Value::Number(n)) + } + DynSolValue::Uint(i, _) => { + // let serde handle number parsing + let n = serde_json::from_str(&i.to_string())?; + Ok(Value::Number(n)) + } + DynSolValue::Address(a) => Ok(Value::String(a.to_string())), + DynSolValue::Array(e) | DynSolValue::FixedArray(e) => { + Ok(Value::Array(e.into_iter().map(serialize_value_as_json).collect::>()?)) + } + DynSolValue::CustomStruct { name: _, prop_names, tuple } => { + let values = + tuple.into_iter().map(serialize_value_as_json).collect::>>()?; + let map = prop_names.into_iter().zip(values).collect(); + + Ok(Value::Object(map)) + } + DynSolValue::Tuple(values) => Ok(Value::Array( + values.into_iter().map(serialize_value_as_json).collect::>()?, + )), + DynSolValue::Function(_) => eyre::bail!("cannot serialize function pointer"), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/common/fmt/src/lib.rs b/crates/common/fmt/src/lib.rs index f571b1a450fb4..a1792263e76de 100644 --- a/crates/common/fmt/src/lib.rs +++ b/crates/common/fmt/src/lib.rs @@ -6,7 +6,10 @@ mod console; pub use console::{ConsoleFmt, FormatSpec, console_format}; mod dynamic; -pub use dynamic::{format_token, format_token_raw, format_tokens, format_tokens_raw, parse_tokens}; +pub use dynamic::{ + format_token, format_token_raw, format_tokens, format_tokens_raw, parse_tokens, + serialize_value_as_json, +}; mod exp; pub use exp::{format_int_exp, format_uint_exp, to_exp_notation};