Skip to content

feat(cast): bring remaining --json outputs into JsonEnvelope#14918

Open
mablr wants to merge 19 commits into
masterfrom
mablr/cast-json-envelope-parity
Open

feat(cast): bring remaining --json outputs into JsonEnvelope#14918
mablr wants to merge 19 commits into
masterfrom
mablr/cast-json-envelope-parity

Conversation

@mablr
Copy link
Copy Markdown
Collaborator

@mablr mablr commented May 26, 2026

Description

Towards OSS-239 (almost there after wallet follow-up)

Move print_scalar, print_list, print_tokens, and print_json_object from local helpers in args.rs into foundry_cli::json so every cmd module can import them directly instead of inlining the if/else pattern.

Replace all remaining inline if shell::is_json() { sh_println!(...) } patterns across cmd modules:

  • estimate, find_block, call, mktxprint_scalar
  • txpool (all variants), rpc, vaddr/createprint_json_object
  • keychain list/inspect/doctorprint_json_object / print_json_success
  • erc20 name/symbol/decimalsprint_scalar
  • erc20 balance/allowance/total_supplyprint_json_success (keep format_uint_exp for plain-mode display)
  • Block and Tx in args.rs → parse pre-formatted JSON string and wrap
  • Selectors in args.rs → new SelectorInfo struct, envelope-wraps array
  • AbiEncodeEvent in args.rs → new EncodedEvent{topics,data} struct
  • interface → wrap ABI array in envelope on stdout path

Documented explicitly unsupported commands with inline comments: send (N::ReceiptResponse lacks Serialize), logs --subscribe (streaming), wallet (follow-up), upload-signature (external API), four-byte-calldata (interactive).

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes (for commands that already output JSON by default)

Move `print_scalar`, `print_list`, `print_tokens`, and
`print_json_object` from
local helpers in `args.rs` into `foundry_cli::json` so every cmd module
can import them directly instead of inlining the if/else pattern.

Replace all remaining inline `if shell::is_json() { sh_println!(...) }`
patterns across cmd modules:
- `estimate`, `find_block`, `call`, `mktx` → `print_scalar`
- `txpool` (all variants), `rpc`, `vaddr/create` → `print_json_object`
- `keychain list/inspect/doctor` → `print_json_object` /
  `print_json_success`
- `erc20 name/symbol/decimals` → `print_scalar`
- `erc20 balance/allowance/total_supply` → `print_json_success` (keep
  `format_uint_exp` for plain-mode display)
- `Block` and `Tx` in `args.rs` → parse pre-formatted JSON string and
  wrap
- `Selectors` in `args.rs` → new `SelectorInfo` struct, envelope-wraps
  array
- `AbiEncodeEvent` in `args.rs` → new `EncodedEvent{topics,data}` struct
- `interface` → wrap ABI array in envelope on stdout path

Document explicitly unsupported commands with inline comments: `send`
(N::ReceiptResponse lacks Serialize), `logs --subscribe` (streaming),
`wallet` (follow-up), `upload-signature` (external API),
`four-byte-calldata` (interactive).

Add CLI tests: `selectors_json_envelope`,
`abi_encode_event_json_envelope`.
Comment thread crates/cast/src/args.rs Outdated
Comment thread crates/cast/src/cmd/call.rs Outdated
Comment thread crates/cast/src/cmd/interface.rs
Copy link
Copy Markdown
Member

@mattsse mattsse left a comment

Choose a reason for hiding this comment

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

Requesting changes: these paths can fail or emit invalid JSON for valid --json usages.

P1: cast call --json reparses response as JSON, but Cast::call returns raw hex when no output is decoded. Valid calls can therefore error on serde_json::from_str. Please avoid reparsing arbitrary display output; return structured data from Cast::call, or wrap raw hex as a JSON string/envelope in the command layer.

P1: cast block --json and cast tx --json also assume output is a JSON object. --raw or scalar field paths such as block hash / tx from can return scalar strings, which either fail JSON parsing or are not objects. Please only call print_json_object for object output, and use a scalar JSON envelope/string path for field selections.

P2: cast mktx prints the expiration status line to stdout before later JSON/scalar output, which corrupts --json output. Please move this to stderr/warning output, suppress it under shell::is_json(), or include it inside the JSON payload.

Please add regressions for cast call --json with undecoded/raw output, cast block/tx --json scalar field paths, and cast mktx --json --expires-at ... producing a single valid JSON document.

Copy link
Copy Markdown
Collaborator Author

@mablr mablr left a comment

Choose a reason for hiding this comment

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

Outscoped cast call as it was overlapping with #14774.

Fixed the rest.

@mablr mablr force-pushed the mablr/cast-json-envelope-parity branch from 959137a to 60cfa08 Compare May 28, 2026 08:39
@mablr mablr requested review from figtracer and mattsse May 28, 2026 08:40
Copy link
Copy Markdown
Collaborator

@figtracer figtracer left a comment

Choose a reason for hiding this comment

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

Three focused JSON-output regressions from the local review pass.

Comment thread crates/cast/src/cmd/vaddr/create.rs
Comment thread crates/cast/src/cmd/keychain.rs
Comment thread crates/cast/src/cmd/interface.rs Outdated
- args.rs: use get(pos).cloned() instead of indexing resolve_results to
  avoid panic in cast selectors --json when --resolve is not set
- interface.rs: use sh_status! for "Saved interface at" so it goes to
  stderr in JSON mode, keeping stdout clean
- keychain.rs: move is_json() branch before the is_empty() guard so
  cast --json keychain list emits [] instead of plain text when empty
- vaddr/create.rs: defer JSON/text output until after register()
  succeeds
  to avoid emitting two stdout documents when registration fails
Copy link
Copy Markdown
Collaborator

@stevencartavia stevencartavia left a comment

Choose a reason for hiding this comment

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

crates/cast/src/cmd/vaddr/create.rs — mined salt is lost on register failure

The non---no-register path now calls register(...).await? before printing the salt/registration_hash/master_id/virtual_addresses block. On master those values were printed first.

Reproduced with no signer (so register errors):

master

STDOUT:
Salt:              0x9bf4…155f
Registration hash: 0x0000…e5fd
Master ID:         0x3a96b610
Virtual addresses:
  tag=0x000000000000  0x3A96B610…0000
STDERR:
Error: cast vaddr create requires a signer (...)

PR

STDOUT:  (only the "Mining…/Found salt in…" lines)
STDERR:
Error: cast vaddr create requires a signer (...)

The mined salt is now lost on any registration failure — the user has to mine again (which can take minutes). Suggest restoring the prior order: compute the payload once, print it, then call register. That also lets you collapse the duplicated JSON/plain-text blocks between the no_register branch and the register branch (both currently emit the same payload; print_json_success and print_json_object are equivalent inside if shell::is_json()).


crates/cast/src/cmd/keychain.rsrun_list empty case becomes an error

run_list now routes the empty-keys case through sh_err!, which writes to stderr and prefixes Error:. The same situation in run_show still uses sh_println!, so the two siblings disagree.

Reproduced with TEMPO_HOME pointing at a keys.toml containing keys = []:

master:  EXIT 0  STDOUT: "No keys found in keys.toml."  STDERR: (empty)
PR:      EXIT 0  STDOUT: (empty)                        STDERR: "Error: No keys found in keys.toml."

An empty list is a successful result, not an error. Suggest keeping sh_println! here for parity with run_show (or change both).


crates/cast/src/cmd/interface.rs--json stdout and --json -o file shapes diverge

cast interface --json to stdout now wraps the parsed ABIs in a JsonEnvelope, but cast interface --json -o <file> still writes the raw concatenated json_abi strings to the file. Same data, two shapes depending on whether -o is set.

Reproduced with a single-ABI input:

stdout (--json) file (--json -o)
master [ { ...abi... } ] [ { ...abi... } ]
PR {"schema_version":1,"success":true,"data":[[{...}]],…} [ { ...abi... } ]

If file output is intentionally a raw artifact (not wrapped), please call that out (doc/comment + test); otherwise both destinations should share the envelope shape. Note that for multiple interfaces the file content is also several JSON objects newline-joined, which isn't one valid JSON document.

@mablr mablr requested review from figtracer and stevencartavia May 28, 2026 18:34
Comment thread crates/cast/src/args.rs Outdated
Comment thread crates/cast/src/cmd/vaddr/create.rs Outdated
stevencartavia
stevencartavia previously approved these changes May 28, 2026
figtracer
figtracer previously approved these changes May 29, 2026
Copy link
Copy Markdown
Collaborator

@grandizzy grandizzy left a comment

Choose a reason for hiding this comment

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

A few things to address before merge, plus some smaller suggestions.

Blocking

1. cast block/cast tx with --raw or --field will double-encode JSON in --json mode

In args.rs, only the default whole-object branch parses output to Value before wrapping:

if shell::is_json() && !is_raw_block && !has_fields {
    print_json_object(serde_json::from_str::<Value>(&output)?)?;
} else {
    print_scalar(output)?;
}

That means cast tx --json --field authorizationList, cast block --json --raw, and any field whose value is itself an object/array will emit:

{ "data": "{\"some\":\"json\"}" }

instead of a real nested object. Suggest a small helper that tries serde_json::from_str::<Value> and falls back to print_scalar on parse error, and using it for both Block and Tx. Worth adding tests for --raw and an object-valued --field.

2. keychain doctor --json drops the payload's own schema_version

The envelope's schema_version isn't the same thing as the doctor report schema — anything keying off the inner version will silently break. Please keep schema_version inside data alongside context/steps.

3. Build hygiene

  • interface.rs uses bare Value in serde_json::from_str::<Value>(&iface.json_abi) — I don't see a use serde_json::Value; added. Either add the import or use serde_json::Value consistently with the rest of the file.
  • foundry_cli::json now imports alloy_dyn_abi::DynSolValue. If foundry-cli's Cargo.toml didn't already list alloy-dyn-abi as a direct dep, please add it rather than relying on a transitive.

Non-blocking suggestions

  • Naming: print_json_object is also used for arrays and scalars — print_structured or print_json_value would describe it better. Easier to rename now than later.
  • cast rpc plain mode: the new path always parses and pretty-prints via print_json_object, which changes non-JSON stdout formatting too. If that's intentional, ignore; otherwise keep the old sh_println!("{result}") branch for !is_json().
  • mktx --expires-at: moving the "Transaction expires at..." line from sh_println! to sh_status! cleans up --json stdout (good) but also changes plain-mode stdout. Consider branching on is_json() if plain-mode parity matters.
  • interface --json: wrapping always in an array means single-interface output gains a level of nesting compared to before — worth an explicit callout in the PR description's breaking-change list.
  • vaddr create --json: the reorder so registration runs before printing is a nice fix. Worth confirming register(...) itself doesn't print to stdout, and adding a JSON test for the with-register path (not just --no-register).
  • SelectorInfo: arguments as a comma-separated string preserves human output but an array (["uint256","address"]) would be friendlier for machine consumers. Also consider stateMutability to match Solidity ABI casing. Non-blocking since this is a new shape and you can iterate.

Suggested additional tests

  • cast block --json (default + --raw + scalar field + object field)
  • cast tx --json --field authorizationList (or any non-scalar field)
  • cast rpc --json against a scalar response like eth_blockNumber
  • cast keychain doctor --json asserting data.schema_version
  • cast interface --json for a single ABI (locks in the array shape)
  • cast vaddr create --json without --no-register

@mablr mablr dismissed stale reviews from figtracer and stevencartavia via fefa399 May 29, 2026 16:30
Copy link
Copy Markdown
Collaborator Author

@mablr mablr left a comment

Choose a reason for hiding this comment

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

@grandizzy I addressed your comment in fefa399.

I think 2. is not necessary, I just added a test to assert json shape as recommended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants