Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/tempo-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ echo "$KC_INFO" | grep -q "secp256k1"

echo -e "\n=== CAST KEYCHAIN: KEY-INFO --json ==="
KC_INFO_JSON=$(cast keychain info "$ADDR" "$KC_KEY_ADDR" --rpc-url "$ETH_RPC_URL" --json)
echo "$KC_INFO_JSON" | jq -e '.signatureType == "secp256k1"'
echo "$KC_INFO_JSON" | jq -e '.data.signatureType == "secp256k1"'

echo -e "\n=== CAST KEYCHAIN: AUTHORIZE WITH LIMIT ==="
kc_limited_json="$(cast wallet new --json)"
Expand Down
148 changes: 65 additions & 83 deletions crates/cast/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
traces::identifier::SignaturesIdentifier,
tx::CastTxSender,
};
use alloy_dyn_abi::{DynSolValue, ErrorExt, EventExt};
use alloy_dyn_abi::{ErrorExt, EventExt};
use alloy_eips::eip7702::SignedAuthorization;
use alloy_ens::{ProviderEnsExt, namehash};
use alloy_network::Ethereum;
Expand All @@ -17,12 +17,12 @@ use clap::CommandFactory;
use clap_complete::generate;
use eyre::{Result, WrapErr};
use foundry_cli::{
json::print_json_success,
json::{print_json_object, print_json_value_or_scalar, print_list, print_scalar, print_tokens},
utils::{self, LoadConfig},
};
use foundry_common::{
abi::{get_error, get_event},
fmt::{format_tokens, format_uint_exp, serialize_value_as_json},
fmt::format_uint_exp,
fs,
provider::ProviderBuilder,
selectors::{
Expand Down Expand Up @@ -242,11 +242,24 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
// envelope
CastSubcommand::AbiEncodeEvent { sig, args } => {
let log_data = SimpleCast::abi_encode_event(&sig, &args)?;
for (i, topic) in log_data.topics().iter().enumerate() {
sh_println!("[topic{}]: {}", i, topic)?;
}
if !log_data.data.is_empty() {
sh_println!("[data]: {}", hex::encode_prefixed(log_data.data))?;
if shell::is_json() {
#[derive(serde::Serialize)]
struct EncodedEvent {
topics: Vec<String>,
data: String,
}
let encoded = EncodedEvent {
topics: log_data.topics().iter().map(|t| t.to_string()).collect(),
data: hex::encode_prefixed(&log_data.data),
};
print_json_object(encoded)?;
} else {
for (i, topic) in log_data.topics().iter().enumerate() {
sh_println!("[topic{}]: {}", i, topic)?;
}
if !log_data.data.is_empty() {
sh_println!("[data]: {}", hex::encode_prefixed(log_data.data))?;
}
}
}
CastSubcommand::DecodeCalldata { sig, calldata, file } => {
Expand Down Expand Up @@ -398,7 +411,8 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
CastSubcommand::Block { block, full, fields, raw, rpc, network } => {
let config = rpc.load_config()?;
// Can use either --raw or specify raw as a field
let output = if raw || fields.contains(&"raw".into()) {
let is_raw_block = raw || fields.contains(&"raw".into());
let output = if is_raw_block {
match network {
#[cfg(feature = "optimism")]
Some(NetworkVariant::Optimism) => {
Expand Down Expand Up @@ -431,8 +445,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
.block(block.unwrap_or(BlockId::Number(Latest)), full, fields)
.await?
};
// JSON: Output is already formatted by `Cast::block()`
sh_println!("{output}")?;
print_json_value_or_scalar(output)?;
}
CastSubcommand::BlockNumber { rpc, block } => {
let config = rpc.load_config()?;
Expand Down Expand Up @@ -505,14 +518,11 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
let out = SimpleCast::disassemble(&hex::decode(bytecode)?)?;
print_scalar(out)?;
}
// TODO(json): tabular multi-row output, needs array envelope
CastSubcommand::Selectors { bytecode, resolve } => {
let bytecode = stdin::unwrap_line(bytecode)?;
let functions = SimpleCast::extract_functions(&bytecode)?;
let max_args_len = functions.iter().map(|r| r.1.len()).max().unwrap_or(0);
let max_mutability_len = functions.iter().map(|r| r.2.len()).max().unwrap_or(0);

let resolve_results = if resolve {
let resolve_results: Vec<String> = if resolve {
let selectors = functions
.iter()
.map(|&(selector, ..)| SelectorKind::Function(selector))
Expand All @@ -522,15 +532,41 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
} else {
vec![]
};
for (pos, (selector, arguments, state_mutability)) in functions.into_iter().enumerate()
{
if resolve {
let resolved = &resolve_results[pos];
sh_println!(
"{selector}\t{arguments:max_args_len$}\t{state_mutability:max_mutability_len$}\t{resolved}"
)?
} else {
sh_println!("{selector}\t{arguments:max_args_len$}\t{state_mutability}")?

if shell::is_json() {
#[derive(serde::Serialize)]
struct SelectorInfo {
selector: String,
arguments: String,
state_mutability: String,
#[serde(skip_serializing_if = "Option::is_none")]
resolved: Option<String>,
}
let infos: Vec<SelectorInfo> = functions
.into_iter()
.enumerate()
.map(|(pos, (selector, arguments, state_mutability))| SelectorInfo {
selector: selector.to_string(),
arguments,
state_mutability: state_mutability.to_string(),
resolved: resolve_results.get(pos).cloned(),
})
.collect();
print_json_object(infos)?;
} else {
let max_args_len = functions.iter().map(|r| r.1.len()).max().unwrap_or(0);
let max_mutability_len = functions.iter().map(|r| r.2.len()).max().unwrap_or(0);
for (pos, (selector, arguments, state_mutability)) in
functions.into_iter().enumerate()
{
if resolve {
let resolved = &resolve_results[pos];
sh_println!(
"{selector}\t{arguments:max_args_len$}\t{state_mutability:max_mutability_len$}\t{resolved}"
)?
} else {
sh_println!("{selector}\t{arguments:max_args_len$}\t{state_mutability}")?
}
}
}
}
Expand Down Expand Up @@ -660,8 +696,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
.await?
}
};
// JSON: Output is already formatted by `Cast::transaction()`
sh_println!("{output}")?;
print_json_value_or_scalar(output)?;
}

// 4Byte
Expand All @@ -674,8 +709,8 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
print_list(&sigs)?;
}

// TODO(json): multiple candidates + interactive selection + decoded tokens, needs
// structured envelope
// JSON envelope intentionally unsupported: output combines an interactive selector
// disambiguation step with decoded token output; no single stable shape exists.
CastSubcommand::FourByteCalldata { calldata } => {
let calldata = stdin::unwrap_line(calldata)?;

Expand Down Expand Up @@ -716,7 +751,8 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
}
print_list(&sigs)?;
}
// TODO(json): external API response printed via .describe(), needs structured envelope
// JSON envelope intentionally unsupported: output is a human-readable summary from an
// external selector registry API with no stable machine-readable schema.
CastSubcommand::UploadSignature { signatures } => {
let signatures = stdin::unwrap_vec(signatures)?;
let ParsedSignatures { signatures, abis } = parse_signatures(signatures);
Expand Down Expand Up @@ -886,60 +922,6 @@ pub async fn run_command(args: CastArgs) -> Result<()> {
CastSubcommand::Trace(cmd) => cmd.run().await?,
};

/// Prints a scalar value: JSON envelope in `--json` mode, plain text otherwise.
fn print_scalar(value: impl serde::Serialize + std::fmt::Display) -> Result<()> {
if shell::is_json() {
print_json_success(value)?;
} else {
sh_println!("{value}")?;
}
Ok(())
}

/// Prints a list of serializable items: JSON envelope wrapping an array in `--json` mode,
/// one item per line otherwise.
fn print_list<T: serde::Serialize + std::fmt::Display>(items: &[T]) -> Result<()> {
if shell::is_json() {
print_json_success(items)?;
} else {
for item in items {
sh_println!("{item}")?;
}
}
Ok(())
}

/// Wraps a serializable object in the JSON envelope in `--json` mode, otherwise pretty-prints
/// it as JSON. Used for objects that have no human-readable `Display` format.
fn print_json_object<T: serde::Serialize>(value: T) -> Result<()> {
if shell::is_json() {
print_json_success(value)?;
} else {
sh_println!("{}", serde_json::to_string_pretty(&value)?)?;
}
Ok(())
}

/// 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]) -> Result<()> {
if shell::is_json() {
let values = tokens
.iter()
.cloned()
.map(|t| serialize_value_as_json(t, None))
.collect::<Result<Vec<serde_json::Value>>>()?;
print_json_success(values)?;
} else {
format_tokens(tokens).for_each(|t| {
let _ = sh_println!("{t}");
});
}
Ok(())
}

Ok(())
}

Expand Down
29 changes: 9 additions & 20 deletions crates/cast/src/cmd/erc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use alloy_signer::{Signature, Signer};
use alloy_sol_types::sol;
use clap::Parser;
use foundry_cli::{
json::{print_json_success, print_scalar},
opts::RpcOpts,
utils::{LoadConfig, get_chain, get_provider},
};
Expand Down Expand Up @@ -504,9 +505,9 @@ impl Erc20Subcommand {
.await?;

if shell::is_json() {
sh_println!("{}", serde_json::to_string(&allowance.to_string())?)?
print_json_success(allowance.to_string())?;
} else {
sh_println!("{}", format_uint_exp(allowance))?
sh_println!("{}", format_uint_exp(allowance))?;
}
}
Self::Balance { token, owner, block, .. } => {
Expand All @@ -521,9 +522,9 @@ impl Erc20Subcommand {
.await?;

if shell::is_json() {
sh_println!("{}", serde_json::to_string(&balance.to_string())?)?
print_json_success(balance.to_string())?;
} else {
sh_println!("{}", format_uint_exp(balance))?
sh_println!("{}", format_uint_exp(balance))?;
}
}
Self::Name { token, block, .. } => {
Expand All @@ -536,11 +537,7 @@ impl Erc20Subcommand {
.call()
.await?;

if shell::is_json() {
sh_println!("{}", serde_json::to_string(&name)?)?
} else {
sh_println!("{}", name)?
}
print_scalar(name)?;
}
Self::Symbol { token, block, .. } => {
let provider = get_provider(&config)?;
Expand All @@ -552,11 +549,7 @@ impl Erc20Subcommand {
.call()
.await?;

if shell::is_json() {
sh_println!("{}", serde_json::to_string(&symbol)?)?
} else {
sh_println!("{}", symbol)?
}
print_scalar(symbol)?;
}
Self::Decimals { token, block, .. } => {
let provider = get_provider(&config)?;
Expand All @@ -567,11 +560,7 @@ impl Erc20Subcommand {
.block(block.unwrap_or_default())
.call()
.await?;
if shell::is_json() {
sh_println!("{}", serde_json::to_string(&decimals)?)?
} else {
sh_println!("{}", decimals)?
}
print_scalar(decimals)?;
}
Self::TotalSupply { token, block, .. } => {
let provider = get_provider(&config)?;
Expand All @@ -584,7 +573,7 @@ impl Erc20Subcommand {
.await?;

if shell::is_json() {
sh_println!("{}", serde_json::to_string(&total_supply.to_string())?)?
print_json_success(total_supply.to_string())?;
} else {
sh_println!("{}", format_uint_exp(total_supply))?
}
Expand Down
5 changes: 3 additions & 2 deletions crates/cast/src/cmd/estimate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use alloy_rpc_types::BlockId;
use clap::Parser;
use eyre::Result;
use foundry_cli::{
json::print_scalar,
opts::{RpcOpts, TransactionOpts},
utils::{LoadConfig, parse_ether_value},
};
Expand Down Expand Up @@ -130,9 +131,9 @@ impl EstimateArgs {
let gas_price_wei = provider.get_gas_price().await?;
let cost = gas_price_wei * gas as u128;
let cost_eth = cost as f64 / 1e18;
sh_println!("{cost_eth}")?;
print_scalar(cost_eth)?;
} else {
sh_println!("{gas}")?;
print_scalar(gas)?;
}
Ok(())
}
Expand Down
3 changes: 2 additions & 1 deletion crates/cast/src/cmd/find_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use alloy_provider::Provider;
use clap::Parser;
use eyre::Result;
use foundry_cli::{
json::print_scalar,
opts::RpcOpts,
utils::{self, LoadConfig},
};
Expand Down Expand Up @@ -79,7 +80,7 @@ impl FindBlockArgs {
}
matching_block.unwrap_or(low_block)
};
sh_println!("{block_num}")?;
print_scalar(block_num)?;

Ok(())
}
Expand Down
Loading
Loading