Skip to content

fix(cast): calldata-decode --json nested tuple formatting #11212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
4 changes: 2 additions & 2 deletions crates/cast/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, token_to_json},
fs,
selectors::{
ParsedSignatures, SelectorImportData, SelectorKind, decode_calldata, decode_event_topic,
Expand Down Expand Up @@ -757,7 +757,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
/// 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<String> = format_tokens_raw(tokens).collect();
let tokens: Vec<serde_json::Value> = tokens.iter().map(token_to_json).collect();
let _ = sh_println!("{}", serde_json::to_string_pretty(&tokens).unwrap());
} else {
let tokens = format_tokens(tokens);
Expand Down
43 changes: 42 additions & 1 deletion crates/cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2313,7 +2313,7 @@ fn explorer_client(

#[cfg(test)]
mod tests {
use super::SimpleCast as Cast;
use super::{DynSolValue, SimpleCast as Cast, token_to_json};
use alloy_primitives::hex;

#[test]
Expand Down Expand Up @@ -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 = token_to_json(&DynSolValue::Array(decoded));
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");
Expand Down
27 changes: 27 additions & 0 deletions crates/common/fmt/src/dynamic.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{format_int_exp, format_uint_exp};
use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_primitives::hex;
use serde_json::{Value, json};
use std::fmt;

/// [`DynSolValue`] formatter.
Expand Down Expand Up @@ -146,6 +147,32 @@ pub fn format_token_raw(value: &DynSolValue) -> String {
DynValueDisplay::new(value, true).to_string()
}

/// Recursively converts a `DynSolValue` into a serde_json::Value.
pub fn token_to_json(value: &DynSolValue) -> Value {
Copy link
Contributor

@0xrusowsky 0xrusowsky Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this PR may be a chance to consolidate implementations, as we also have

fn serialize_value_as_json(value: DynSolValue) -> Result<Value> {

thoughts @DaniPopes @zerosnacks ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, serialize_value_as_json is a robust implementation that could be reused

match value {
DynSolValue::Array(values)
| DynSolValue::FixedArray(values)
| DynSolValue::Tuple(values) => {
let tokens: Vec<Value> = values.iter().map(token_to_json).collect();
Value::Array(tokens)
}
DynSolValue::CustomStruct { name, prop_names, tuple } => {
if prop_names.len() == tuple.len() {
let obj = prop_names
.iter()
.zip(tuple)
.map(|(k, v)| (k.clone(), token_to_json(v)))
.collect();
Value::Object(obj)
} else {
let tokens: Vec<Value> = tuple.iter().map(token_to_json).collect();
json!({ name: tokens })
}
}
_ => json!(format_token_raw(value)),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
4 changes: 3 additions & 1 deletion crates/common/fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ 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, token_to_json,
};

mod exp;
pub use exp::{format_int_exp, format_uint_exp, to_exp_notation};
Expand Down
Loading