Skip to main content

cast/
args.rs

1use crate::{
2    Cast, SimpleCast,
3    cmd::erc20::IERC20,
4    introspect::REGISTRY,
5    opts::{Cast as CastArgs, CastSubcommand, ToBaseArgs},
6    traces::identifier::SignaturesIdentifier,
7    tx::CastTxSender,
8};
9use alloy_dyn_abi::{ErrorExt, EventExt};
10use alloy_eips::eip7702::SignedAuthorization;
11use alloy_ens::{ProviderEnsExt, namehash};
12use alloy_network::{Ethereum, eip2718::Decodable2718};
13use alloy_primitives::{Address, B256, eip191_hash_message, hex, keccak256};
14use alloy_provider::Provider;
15use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest};
16use clap::CommandFactory;
17use clap_complete::generate;
18use eyre::{Result, WrapErr};
19use foundry_cli::{
20    json::{print_json_object, print_json_value_or_scalar, print_list, print_scalar, print_tokens},
21    utils::{self, LoadConfig},
22};
23use foundry_common::{
24    abi::{get_error, get_event},
25    fmt::format_uint_exp,
26    fs,
27    provider::ProviderBuilder,
28    selectors::{
29        ParsedSignatures, SelectorImportData, SelectorKind, decode_calldata, decode_event_topic,
30        decode_function_selector, decode_selectors, import_selectors, parse_signatures,
31        pretty_calldata,
32    },
33    shell, stdin,
34};
35use foundry_evm_networks::NetworkVariant;
36use foundry_primitives::{FoundryTxEnvelope, PaymentLaneClassification};
37#[cfg(feature = "optimism")]
38use op_alloy_network::Optimism;
39use std::time::Instant;
40use tempo_alloy::TempoNetwork;
41use tempo_contracts::precompiles::{ITIP20ChannelReserve, TIP20_CHANNEL_RESERVE_ADDRESS};
42
43/// Run the `cast` command-line interface.
44pub fn run() -> Result<()> {
45    // Pre-parse discovery flags run before `setup()` so they cannot be blocked
46    // by panic-handler / tracing init failures and avoid that init's cost.
47    foundry_cli::machine::check_machine();
48    foundry_cli::opts::GlobalArgs::check_introspect_with(CastArgs::command, &REGISTRY);
49    foundry_cli::opts::GlobalArgs::check_markdown_help::<CastArgs>();
50
51    setup()?;
52
53    let args = foundry_cli::parse_or_exit::<CastArgs>();
54    args.global.init()?;
55    args.global.tokio_runtime().block_on(run_command(args))
56}
57
58/// Setup the global logger and other utilities.
59pub fn setup() -> Result<()> {
60    utils::common_setup();
61    utils::subscriber();
62
63    Ok(())
64}
65
66/// Run the subcommand.
67#[allow(clippy::large_stack_frames)]
68pub async fn run_command(args: CastArgs) -> Result<()> {
69    match args.cmd {
70        // Constants
71        CastSubcommand::MaxInt { r#type } => {
72            let out = SimpleCast::max_int(&r#type)?;
73            print_scalar(out)?;
74        }
75        CastSubcommand::MinInt { r#type } => {
76            let out = SimpleCast::min_int(&r#type)?;
77            print_scalar(out)?;
78        }
79        CastSubcommand::MaxUint { r#type } => {
80            let out = SimpleCast::max_int(&r#type)?;
81            print_scalar(out)?;
82        }
83        CastSubcommand::AddressZero => {
84            print_scalar(format!("{:?}", Address::ZERO))?;
85        }
86        CastSubcommand::HashZero => {
87            print_scalar(format!("{:?}", B256::ZERO))?;
88        }
89
90        // Conversions & transformations
91        CastSubcommand::FromUtf8 { text } => {
92            let value = stdin::unwrap(text, false)?;
93            let out = SimpleCast::from_utf8(&value);
94            print_scalar(out)?;
95        }
96        CastSubcommand::ToAscii { hexdata } => {
97            let value = stdin::unwrap(hexdata, false)?;
98            let out = SimpleCast::to_ascii(value.trim())?;
99            print_scalar(out)?;
100        }
101        CastSubcommand::ToUtf8 { hexdata } => {
102            let value = stdin::unwrap(hexdata, false)?;
103            let out = SimpleCast::to_utf8(&value)?;
104            print_scalar(out)?;
105        }
106        CastSubcommand::FromFixedPoint { value, decimals } => {
107            let (value, decimals) = stdin::unwrap2(value, decimals)?;
108            let out = SimpleCast::from_fixed_point(&value, &decimals)?;
109            print_scalar(out)?;
110        }
111        CastSubcommand::ToFixedPoint { value, decimals } => {
112            let (value, decimals) = stdin::unwrap2(value, decimals)?;
113            let out = SimpleCast::to_fixed_point(&value, &decimals)?;
114            print_scalar(out)?;
115        }
116        CastSubcommand::ConcatHex { data } => {
117            let out = if data.is_empty() {
118                let s = stdin::read(true)?;
119                SimpleCast::concat_hex(s.split_whitespace())
120            } else {
121                SimpleCast::concat_hex(data)
122            };
123            print_scalar(out)?;
124        }
125        CastSubcommand::FromBin => {
126            let hex = stdin::read_bytes(false)?;
127            let out = hex::encode_prefixed(hex);
128            print_scalar(out)?;
129        }
130        CastSubcommand::ToHexdata { input } => {
131            let value = stdin::unwrap_line(input)?;
132            let output = match value {
133                s if s.starts_with('@') => hex::encode(std::env::var(&s[1..])?),
134                s if s.starts_with('/') => hex::encode(fs::read(s)?),
135                s => s.split(':').map(|s| s.trim_start_matches("0x").to_lowercase()).collect(),
136            };
137            print_scalar(format!("0x{output}"))?;
138        }
139        CastSubcommand::ToCheckSumAddress { address, chain_id } => {
140            let value = stdin::unwrap_line(address)?;
141            let out = value.to_checksum(chain_id);
142            print_scalar(out)?;
143        }
144        CastSubcommand::ToUint256 { value } => {
145            let value = stdin::unwrap_line(value)?;
146            let out = SimpleCast::to_uint256(&value)?;
147            print_scalar(out)?;
148        }
149        CastSubcommand::ToInt256 { value } => {
150            let value = stdin::unwrap_line(value)?;
151            let out = SimpleCast::to_int256(&value)?;
152            print_scalar(out)?;
153        }
154        CastSubcommand::ToUnit { value, unit } => {
155            let value = stdin::unwrap_line(value)?;
156            let out = SimpleCast::to_unit(&value, &unit)?;
157            print_scalar(out)?;
158        }
159        CastSubcommand::ParseUnits { value, unit } => {
160            let value = stdin::unwrap_line(value)?;
161            let out = SimpleCast::parse_units(&value, unit)?;
162            print_scalar(out)?;
163        }
164        CastSubcommand::FormatUnits { value, unit } => {
165            let value = stdin::unwrap_line(value)?;
166            let out = SimpleCast::format_units(&value, unit)?;
167            print_scalar(out)?;
168        }
169        CastSubcommand::FromWei { value, unit } => {
170            let value = stdin::unwrap_line(value)?;
171            let out = SimpleCast::from_wei(&value, &unit)?;
172            print_scalar(out)?;
173        }
174        CastSubcommand::ToWei { value, unit } => {
175            let value = stdin::unwrap_line(value)?;
176            let out = SimpleCast::to_wei(&value, &unit)?;
177            print_scalar(out)?;
178        }
179        CastSubcommand::FromRlp { value, as_int } => {
180            let value = stdin::unwrap_line(value)?;
181            let out = SimpleCast::from_rlp(value, as_int)?;
182            print_scalar(out)?;
183        }
184        CastSubcommand::ToRlp { value } => {
185            let value = stdin::unwrap_line(value)?;
186            let out = SimpleCast::to_rlp(&value)?;
187            print_scalar(out)?;
188        }
189        CastSubcommand::ToHex(ToBaseArgs { value, base_in }) => {
190            let value = stdin::unwrap_line(value)?;
191            let out = SimpleCast::to_base(&value, base_in.as_deref(), "hex")?;
192            print_scalar(out)?;
193        }
194        CastSubcommand::ToDec(ToBaseArgs { value, base_in }) => {
195            let value = stdin::unwrap_line(value)?;
196            let out = SimpleCast::to_base(&value, base_in.as_deref(), "dec")?;
197            print_scalar(out)?;
198        }
199        CastSubcommand::ToBase { base: ToBaseArgs { value, base_in }, base_out } => {
200            let (value, base_out) = stdin::unwrap2(value, base_out)?;
201            let out = SimpleCast::to_base(&value, base_in.as_deref(), &base_out)?;
202            print_scalar(out)?;
203        }
204        CastSubcommand::ToBytes32 { bytes } => {
205            let value = stdin::unwrap_line(bytes)?;
206            let out = SimpleCast::to_bytes32(&value)?;
207            print_scalar(out)?;
208        }
209        CastSubcommand::Pad { data, right, left: _, len } => {
210            let value = stdin::unwrap_line(data)?;
211            let out = SimpleCast::pad(&value, right, len)?;
212            print_scalar(out)?;
213        }
214        CastSubcommand::FormatBytes32String { string } => {
215            let value = stdin::unwrap_line(string)?;
216            let out = SimpleCast::format_bytes32_string(&value)?;
217            print_scalar(out)?;
218        }
219        CastSubcommand::ParseBytes32String { bytes } => {
220            let value = stdin::unwrap_line(bytes)?;
221            let out = SimpleCast::parse_bytes32_string(&value)?;
222            print_scalar(out)?;
223        }
224        CastSubcommand::ParseBytes32Address { bytes } => {
225            let value = stdin::unwrap_line(bytes)?;
226            let out = SimpleCast::parse_bytes32_address(&value)?;
227            print_scalar(out)?;
228        }
229
230        // ABI encoding & decoding
231        CastSubcommand::DecodeAbi { sig, calldata, input } => {
232            let tokens = SimpleCast::abi_decode(&sig, &calldata, input)?;
233            print_tokens(&tokens)?;
234        }
235        CastSubcommand::AbiEncode { sig, packed, args } => {
236            let out = if packed {
237                SimpleCast::abi_encode_packed(&sig, &args)?
238            } else {
239                SimpleCast::abi_encode(&sig, &args)?
240            };
241            print_scalar(out)?;
242        }
243        // TODO(json): multi-line output (one line per topic + data field), needs structured object
244        // envelope
245        CastSubcommand::AbiEncodeEvent { sig, args } => {
246            let log_data = SimpleCast::abi_encode_event(&sig, &args)?;
247            if shell::is_json() {
248                #[derive(serde::Serialize)]
249                struct EncodedEvent {
250                    topics: Vec<String>,
251                    data: String,
252                }
253                let encoded = EncodedEvent {
254                    topics: log_data.topics().iter().map(|t| t.to_string()).collect(),
255                    data: hex::encode_prefixed(&log_data.data),
256                };
257                print_json_object(encoded)?;
258            } else {
259                for (i, topic) in log_data.topics().iter().enumerate() {
260                    sh_println!("[topic{}]: {}", i, topic)?;
261                }
262                if !log_data.data.is_empty() {
263                    sh_println!("[data]: {}", hex::encode_prefixed(log_data.data))?;
264                }
265            }
266        }
267        CastSubcommand::DecodeCalldata { sig, calldata, file } => {
268            let raw_hex = if let Some(file_path) = file {
269                let contents = fs::read_to_string(&file_path)?;
270                contents.trim().to_string()
271            } else {
272                calldata.unwrap()
273            };
274
275            let tokens = SimpleCast::calldata_decode(&sig, &raw_hex, true)?;
276            print_tokens(&tokens)?;
277        }
278        CastSubcommand::CalldataEncode { sig, args, file } => {
279            let final_args = if let Some(file_path) = file {
280                let contents = fs::read_to_string(file_path)?;
281                contents
282                    .lines()
283                    .map(str::trim)
284                    .filter(|line| !line.is_empty())
285                    .map(String::from)
286                    .collect()
287            } else {
288                args
289            };
290            let out = SimpleCast::calldata_encode(sig, &final_args)?;
291            print_scalar(out)?;
292        }
293        CastSubcommand::DecodeString { data } => {
294            let tokens = SimpleCast::calldata_decode("Any(string)", &data, true)?;
295            print_tokens(&tokens)?;
296        }
297        CastSubcommand::DecodeEvent { sig, data } => {
298            let decoded_event = if let Some(event_sig) = sig {
299                let event = get_event(event_sig.as_str())?;
300                event.decode_log_parts(core::iter::once(event.selector()), &hex::decode(data)?)?
301            } else {
302                let data = crate::strip_0x(&data);
303                let selector = data.get(..64).unwrap_or_default();
304                let selector = selector.parse()?;
305                let identified_event =
306                    SignaturesIdentifier::new(false)?.identify_event(selector).await;
307                if let Some(event) = identified_event {
308                    let _ = sh_println!("{}", event.signature());
309                    let data = data.get(64..).unwrap_or_default();
310                    get_event(event.signature().as_str())?
311                        .decode_log_parts(core::iter::once(selector), &hex::decode(data)?)?
312                } else {
313                    eyre::bail!("No matching event signature found for selector `{selector}`")
314                }
315            };
316            print_tokens(&decoded_event.body)?;
317        }
318        CastSubcommand::DecodeError { sig, data } => {
319            let error = if let Some(err_sig) = sig {
320                get_error(err_sig.as_str())?
321            } else {
322                let data = crate::strip_0x(&data);
323                let selector = data.get(..8).unwrap_or_default();
324                let identified_error =
325                    SignaturesIdentifier::new(false)?.identify_error(selector.parse()?).await;
326                if let Some(error) = identified_error {
327                    let _ = sh_println!("{}", error.signature());
328                    error
329                } else {
330                    eyre::bail!("No matching error signature found for selector `{selector}`")
331                }
332            };
333            let decoded_error = error.decode_error(&hex::decode(data)?)?;
334            print_tokens(&decoded_error.body)?;
335        }
336        CastSubcommand::Interface(cmd) => cmd.run().await?,
337        CastSubcommand::CreationCode(cmd) => cmd.run().await?,
338        CastSubcommand::ConstructorArgs(cmd) => cmd.run().await?,
339        CastSubcommand::Artifact(cmd) => cmd.run().await?,
340        CastSubcommand::Bind(cmd) => cmd.run().await?,
341        CastSubcommand::B2EPayload(cmd) => cmd.run().await?,
342        CastSubcommand::PrettyCalldata { calldata, offline } => {
343            let calldata = stdin::unwrap_line(calldata)?;
344            let out = pretty_calldata(&calldata, offline).await?.to_string();
345            print_scalar(out)?;
346        }
347        // JSON: --optimize conflicts with --json at the clap level; optimize=None uses print_scalar
348        CastSubcommand::Sig { sig, optimize } => {
349            let sig = stdin::unwrap_line(sig)?;
350            match optimize {
351                Some(opt) => {
352                    sh_status!("Starting to optimize signature...")?;
353                    let start_time = Instant::now();
354                    let (selector, signature) = SimpleCast::get_selector(&sig, opt)?;
355                    sh_status!("Successfully generated in {:?}", start_time.elapsed())?;
356                    sh_println!("Selector: {selector}")?;
357                    sh_println!("Optimized signature: {signature}")?;
358                }
359                None => {
360                    let out = SimpleCast::get_selector(&sig, 0)?.0;
361                    print_scalar(out)?;
362                }
363            }
364        }
365
366        // Blockchain & RPC queries
367        CastSubcommand::AccessList(cmd) => cmd.run().await?,
368        CastSubcommand::Age { block, rpc } => {
369            let config = rpc.load_config()?;
370            let provider = utils::get_provider(&config)?;
371            let out = format!(
372                "{} UTC",
373                Cast::new(provider).age(block.unwrap_or(BlockId::Number(Latest))).await?
374            );
375            print_scalar(out)?;
376        }
377        CastSubcommand::Balance { block, who, ether, rpc, erc20 } => {
378            let config = rpc.load_config()?;
379            let provider = utils::get_provider(&config)?;
380            let account_addr = who.resolve(&provider).await?;
381
382            match erc20 {
383                Some(token) => {
384                    let balance = IERC20::new(token, &provider)
385                        .balanceOf(account_addr)
386                        .block(block.unwrap_or_default())
387                        .call()
388                        .await?;
389
390                    sh_warn!("--erc20 flag is deprecated, use `cast erc20 balance` instead")?;
391                    print_scalar(format_uint_exp(balance))?;
392                }
393                None => {
394                    let value = Cast::new(&provider).balance(account_addr, block).await?;
395                    let out = if ether {
396                        SimpleCast::from_wei(&value.to_string(), "eth")?
397                    } else {
398                        value.to_string()
399                    };
400                    print_scalar(out)?;
401                }
402            }
403        }
404        CastSubcommand::BaseFee { block, rpc } => {
405            let config = rpc.load_config()?;
406            let provider = utils::get_provider(&config)?;
407            let out = Cast::new(provider)
408                .base_fee(block.unwrap_or(BlockId::Number(Latest)))
409                .await?
410                .to_string();
411            print_scalar(out)?;
412        }
413        CastSubcommand::Block { block, full, fields, raw, rpc, network } => {
414            let config = rpc.load_config()?;
415            // Can use either --raw or specify raw as a field
416            let is_raw_block = raw || fields.contains(&"raw".into());
417            let output = if is_raw_block {
418                match network {
419                    #[cfg(feature = "optimism")]
420                    Some(NetworkVariant::Optimism) => {
421                        let provider =
422                            ProviderBuilder::<Optimism>::from_config(&config)?.build()?;
423
424                        Cast::new(&provider)
425                            .block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
426                            .await?
427                    }
428                    Some(NetworkVariant::Tempo) => {
429                        let provider =
430                            ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
431                        Cast::new(&provider)
432                            .block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
433                            .await?
434                    }
435                    // Ethereum (default) or no --raw flag
436                    _ => {
437                        let provider =
438                            ProviderBuilder::<Ethereum>::from_config(&config)?.build()?;
439                        Cast::new(&provider)
440                            .block_raw(block.unwrap_or(BlockId::Number(Latest)), full)
441                            .await?
442                    }
443                }
444            } else {
445                let provider = utils::get_provider(&config)?;
446                Cast::new(provider)
447                    .block(block.unwrap_or(BlockId::Number(Latest)), full, fields)
448                    .await?
449            };
450            print_json_value_or_scalar(output)?;
451        }
452        CastSubcommand::BlockNumber { rpc, block } => {
453            let config = rpc.load_config()?;
454            let provider = utils::get_provider(&config)?;
455            let number = match block {
456                Some(id) => {
457                    provider
458                        .get_block(id)
459                        .await?
460                        .ok_or_else(|| eyre::eyre!("block {id:?} not found"))?
461                        .header
462                        .number
463                }
464                None => Cast::new(provider).block_number().await?,
465            };
466            print_scalar(number)?;
467        }
468        CastSubcommand::Chain { rpc } => {
469            let config = rpc.load_config()?;
470            let provider = utils::get_provider(&config)?;
471            let out = Cast::new(provider).chain().await?.to_string();
472            print_scalar(out)?;
473        }
474        CastSubcommand::ChainId { rpc } => {
475            let config = rpc.load_config()?;
476            let provider = utils::get_provider(&config)?;
477            let out = Cast::new(provider).chain_id().await?.to_string();
478            print_scalar(out)?;
479        }
480        CastSubcommand::Client { rpc } => {
481            let config = rpc.load_config()?;
482            let provider = utils::get_provider(&config)?;
483            let out = provider.get_client_version().await?;
484            print_scalar(out)?;
485        }
486        CastSubcommand::Code { block, who, disassemble, rpc } => {
487            let config = rpc.load_config()?;
488            let provider = utils::get_provider(&config)?;
489            let who = who.resolve(&provider).await?;
490            let out = Cast::new(provider).code(who, block, disassemble).await?;
491            print_scalar(out)?;
492        }
493        CastSubcommand::Codesize { block, who, rpc } => {
494            let config = rpc.load_config()?;
495            let provider = utils::get_provider(&config)?;
496            let who = who.resolve(&provider).await?;
497            let out = Cast::new(provider).codesize(who, block).await?;
498            print_scalar(out)?;
499        }
500        CastSubcommand::ComputeAddress { address, nonce, salt, init_code, init_code_hash, rpc } => {
501            let address = stdin::unwrap_line(address)?;
502            let computed = {
503                // For CREATE2, init_code_hash is needed to compute the address
504                if let Some(init_code_hash) = init_code_hash {
505                    address.create2(salt.unwrap_or(B256::ZERO), init_code_hash)
506                } else if let Some(init_code) = init_code {
507                    address.create2(salt.unwrap_or(B256::ZERO), keccak256(hex::decode(init_code)?))
508                } else {
509                    // For CREATE, rpc is needed to compute the address
510                    let config = rpc.load_config()?;
511                    let provider = utils::get_provider(&config)?;
512                    Cast::new(provider).compute_address(address, nonce).await?
513                }
514            };
515            let addr = computed.to_checksum(None);
516            print_scalar(addr)?;
517        }
518        CastSubcommand::Disassemble { bytecode } => {
519            let bytecode = stdin::unwrap_line(bytecode)?;
520            let out = SimpleCast::disassemble(&hex::decode(bytecode)?)?;
521            print_scalar(out)?;
522        }
523        CastSubcommand::Selectors { bytecode, resolve } => {
524            let bytecode = stdin::unwrap_line(bytecode)?;
525            let functions = SimpleCast::extract_functions(&bytecode)?;
526
527            let resolve_results: Vec<String> = if resolve {
528                let selectors = functions
529                    .iter()
530                    .map(|&(selector, ..)| SelectorKind::Function(selector))
531                    .collect::<Vec<_>>();
532                let ds = decode_selectors(&selectors).await?;
533                ds.into_iter().map(|v| v.join("|")).collect()
534            } else {
535                vec![]
536            };
537
538            if shell::is_json() {
539                #[derive(serde::Serialize)]
540                struct SelectorInfo {
541                    selector: String,
542                    arguments: String,
543                    state_mutability: String,
544                    #[serde(skip_serializing_if = "Option::is_none")]
545                    resolved: Option<String>,
546                }
547                let infos: Vec<SelectorInfo> = functions
548                    .into_iter()
549                    .enumerate()
550                    .map(|(pos, (selector, arguments, state_mutability))| SelectorInfo {
551                        selector: selector.to_string(),
552                        arguments,
553                        state_mutability: state_mutability.to_string(),
554                        resolved: resolve_results.get(pos).cloned(),
555                    })
556                    .collect();
557                print_json_object(infos)?;
558            } else {
559                let max_args_len = functions.iter().map(|r| r.1.len()).max().unwrap_or(0);
560                let max_mutability_len = functions.iter().map(|r| r.2.len()).max().unwrap_or(0);
561                for (pos, (selector, arguments, state_mutability)) in
562                    functions.into_iter().enumerate()
563                {
564                    if resolve {
565                        let resolved = &resolve_results[pos];
566                        sh_println!(
567                            "{selector}\t{arguments:max_args_len$}\t{state_mutability:max_mutability_len$}\t{resolved}"
568                        )?
569                    } else {
570                        sh_println!("{selector}\t{arguments:max_args_len$}\t{state_mutability}")?
571                    }
572                }
573            }
574        }
575        CastSubcommand::FindBlock(cmd) => cmd.run().await?,
576        CastSubcommand::GasPrice { rpc } => {
577            let config = rpc.load_config()?;
578            let provider = utils::get_provider(&config)?;
579            let out = Cast::new(provider).gas_price().await?.to_string();
580            print_scalar(out)?;
581        }
582        CastSubcommand::Index { key_type, key, slot_number } => {
583            let out = SimpleCast::index(&key_type, &key, &slot_number)?;
584            print_scalar(out)?;
585        }
586        CastSubcommand::IndexErc7201 { id, formula_id } => {
587            eyre::ensure!(formula_id == "erc7201", "unsupported formula ID: {formula_id}");
588            let id = stdin::unwrap_line(id)?;
589            let out = foundry_common::erc7201(&id).to_string();
590            print_scalar(out)?;
591        }
592        CastSubcommand::Implementation { block, beacon, who, rpc } => {
593            let config = rpc.load_config()?;
594            let provider = utils::get_provider(&config)?;
595            let who = who.resolve(&provider).await?;
596            let out = Cast::new(provider).implementation(who, beacon, block).await?;
597            print_scalar(out)?;
598        }
599        CastSubcommand::Admin { block, who, rpc } => {
600            let config = rpc.load_config()?;
601            let provider = utils::get_provider(&config)?;
602            let who = who.resolve(&provider).await?;
603            let out = Cast::new(provider).admin(who, block).await?;
604            print_scalar(out)?;
605        }
606        CastSubcommand::Nonce { block, who, rpc } => {
607            let config = rpc.load_config()?;
608            let provider = utils::get_provider(&config)?;
609            let who = who.resolve(&provider).await?;
610            let out = Cast::new(provider).nonce(who, block).await?;
611            print_scalar(out)?;
612        }
613        CastSubcommand::Codehash { block, who, slots, rpc } => {
614            let config = rpc.load_config()?;
615            let provider = utils::get_provider(&config)?;
616            let who = who.resolve(&provider).await?;
617            let out = Cast::new(provider).codehash(who, slots, block).await?;
618            print_scalar(out)?;
619        }
620        CastSubcommand::StorageRoot { block, who, slots, rpc } => {
621            let config = rpc.load_config()?;
622            let provider = utils::get_provider(&config)?;
623            let who = who.resolve(&provider).await?;
624            let out = Cast::new(provider).storage_root(who, slots, block).await?;
625            print_scalar(out)?;
626        }
627        CastSubcommand::ChannelId {
628            payer,
629            payee,
630            token,
631            salt,
632            operator,
633            authorized_signer,
634            expiring_nonce_hash,
635            reserve,
636            block,
637            rpc,
638        } => {
639            let config = rpc.load_config()?;
640            let provider = utils::get_provider(&config)?;
641            let payer = payer.resolve(&provider).await?;
642            let payee = payee.resolve(&provider).await?;
643            let token = token.resolve(&provider).await?;
644            let operator = match operator {
645                Some(operator) => operator.resolve(&provider).await?,
646                None => Address::ZERO,
647            };
648            let authorized_signer = match authorized_signer {
649                Some(authorized_signer) => authorized_signer.resolve(&provider).await?,
650                None => Address::ZERO,
651            };
652            let reserve = match reserve {
653                Some(reserve) => reserve.resolve(&provider).await?,
654                None => TIP20_CHANNEL_RESERVE_ADDRESS,
655            };
656
657            let channel_id = ITIP20ChannelReserve::new(reserve, &provider)
658                .computeChannelId(
659                    payer,
660                    payee,
661                    operator,
662                    token,
663                    salt,
664                    authorized_signer,
665                    expiring_nonce_hash,
666                )
667                .block(block.unwrap_or_default())
668                .call()
669                .await?;
670            print_scalar(format!("{channel_id:#x}"))?;
671        }
672        CastSubcommand::Proof { address, slots, rpc, block } => {
673            let config = rpc.load_config()?;
674            let provider = utils::get_provider(&config)?;
675            let address = address.resolve(&provider).await?;
676            let value = provider
677                .get_proof(address, slots.into_iter().collect())
678                .block_id(block.unwrap_or_default())
679                .await?;
680            print_json_object(value)?;
681        }
682        CastSubcommand::Rpc(cmd) => cmd.run().await?,
683        CastSubcommand::Storage(cmd) => cmd.run().await?,
684
685        // Calls & transactions
686        CastSubcommand::Call(cmd) => cmd.run().await?,
687        CastSubcommand::Estimate(cmd) => cmd.run().await?,
688        CastSubcommand::MakeTx(cmd) => cmd.run().await?,
689        CastSubcommand::PublishTx { raw_tx, cast_async, rpc } => {
690            let config = rpc.load_config()?;
691            let provider = utils::get_provider(&config)?;
692            let cast = Cast::new(&provider);
693            let pending_tx = cast.publish(raw_tx).await?;
694            let tx_hash = pending_tx.inner().tx_hash();
695
696            if cast_async {
697                print_scalar(format!("{tx_hash:#x}"))?;
698            } else {
699                let receipt = pending_tx.get_receipt().await?;
700                print_json_object(receipt)?;
701            }
702        }
703        CastSubcommand::Receipt { tx_hash, field, cast_async, confirmations, rpc } => {
704            let config = rpc.load_config()?;
705            let provider = utils::get_provider(&config)?;
706            // JSON: Output is already formatted by `Cast::format_receipt()`
707            sh_println!(
708                "{}",
709                CastTxSender::new(provider)
710                    .receipt(tx_hash, field, confirmations, None, cast_async)
711                    .await?
712            )?
713        }
714        CastSubcommand::Run(cmd) => cmd.run().await?,
715        CastSubcommand::SendTx(cmd) => cmd.run().await?,
716        CastSubcommand::BatchMakeTx(cmd) => cmd.run().await?,
717        CastSubcommand::BatchSend(cmd) => cmd.run().await?,
718        CastSubcommand::Classify { raw_tx } => {
719            let raw_tx = stdin::unwrap_line(raw_tx)?;
720            print_json_value_or_scalar(classify_raw_transaction_output(&raw_tx)?)?
721        }
722        CastSubcommand::Tx { tx_hash, from, nonce, field, raw, lane, rpc, to_request, network } => {
723            let config = rpc.load_config()?;
724            // Can use either --raw or specify raw as a field
725            let is_raw = raw || field.as_ref().is_some_and(|f| f == "raw");
726            let output = match network {
727                #[cfg(feature = "optimism")]
728                Some(NetworkVariant::Optimism) => {
729                    let provider = ProviderBuilder::<Optimism>::from_config(&config)?.build()?;
730
731                    Cast::new(&provider)
732                        .transaction(tx_hash, from, nonce, field, is_raw, to_request, lane)
733                        .await?
734                }
735                Some(NetworkVariant::Tempo) => {
736                    let provider =
737                        ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
738                    Cast::new(&provider)
739                        .transaction(tx_hash, from, nonce, field, is_raw, to_request, lane)
740                        .await?
741                }
742                // Ethereum (default) or no --raw flag
743                _ => {
744                    let provider = utils::get_provider(&config)?;
745                    Cast::new(&provider)
746                        .transaction(tx_hash, from, nonce, field, is_raw, to_request, lane)
747                        .await?
748                }
749            };
750            print_json_value_or_scalar(output)?;
751        }
752
753        // 4Byte
754        CastSubcommand::FourByte { selector } => {
755            let selector = stdin::unwrap_line(selector)?;
756            let sigs = decode_function_selector(selector).await?;
757            if sigs.is_empty() {
758                eyre::bail!("No matching function signatures found for selector `{selector}`");
759            }
760            print_list(&sigs)?;
761        }
762
763        // JSON envelope intentionally unsupported: output combines an interactive selector
764        // disambiguation step with decoded token output; no single stable shape exists.
765        CastSubcommand::FourByteCalldata { calldata } => {
766            let calldata = stdin::unwrap_line(calldata)?;
767
768            if calldata.len() == 10 {
769                let sigs = decode_function_selector(calldata.parse()?).await?;
770                if sigs.is_empty() {
771                    eyre::bail!("No matching function signatures found for calldata `{calldata}`");
772                }
773                for sig in sigs {
774                    sh_println!("{sig}")?
775                }
776                return Ok(());
777            }
778
779            let sigs = decode_calldata(&calldata).await?;
780            sigs.iter().enumerate().for_each(|(i, sig)| {
781                let _ = sh_println!("{}) \"{sig}\"", i + 1);
782            });
783
784            let sig = match sigs.len() {
785                0 => eyre::bail!("No signatures found"),
786                1 => sigs.first().unwrap(),
787                _ => {
788                    let i: usize = prompt!("Select a function signature by number: ")?;
789                    sigs.get(i - 1).ok_or_else(|| eyre::eyre!("Invalid signature index"))?
790                }
791            };
792
793            let tokens = SimpleCast::calldata_decode(sig, &calldata, true)?;
794            print_tokens(&tokens)?;
795        }
796
797        CastSubcommand::FourByteEvent { topic } => {
798            let topic = stdin::unwrap_line(topic)?;
799            let sigs = decode_event_topic(topic).await?;
800            if sigs.is_empty() {
801                eyre::bail!("No matching event signatures found for topic `{topic}`");
802            }
803            print_list(&sigs)?;
804        }
805        // JSON envelope intentionally unsupported: output is a human-readable summary from an
806        // external selector registry API with no stable machine-readable schema.
807        CastSubcommand::UploadSignature { signatures } => {
808            let signatures = stdin::unwrap_vec(signatures)?;
809            let ParsedSignatures { signatures, abis } = parse_signatures(signatures);
810            if !abis.is_empty() {
811                import_selectors(SelectorImportData::Abi(abis)).await?.describe();
812            }
813            if !signatures.is_empty() {
814                import_selectors(SelectorImportData::Raw(signatures)).await?.describe();
815            }
816        }
817
818        // ENS
819        CastSubcommand::Namehash { name } => {
820            let name = stdin::unwrap_line(name)?;
821            let out = namehash(&name).to_string();
822            print_scalar(out)?;
823        }
824        CastSubcommand::LookupAddress { who, rpc, verify } => {
825            let config = rpc.load_config()?;
826            let provider = utils::get_provider(&config)?;
827
828            let who = stdin::unwrap_line(who)?;
829            let name = provider.lookup_address(&who).await?;
830            if verify {
831                let address = provider.resolve_name(&name).await?;
832                eyre::ensure!(
833                    address == who,
834                    "Reverse lookup verification failed: got `{address}`, expected `{who}`"
835                );
836            }
837            print_scalar(name)?;
838        }
839        CastSubcommand::ResolveName { who, rpc, verify } => {
840            let config = rpc.load_config()?;
841            let provider = utils::get_provider(&config)?;
842
843            let who = stdin::unwrap_line(who)?;
844            let address = provider
845                .resolve_name(&who)
846                .await
847                .wrap_err(format!("Failed to resolve ENS name: {who}"))?;
848            if verify {
849                let name = provider.lookup_address(&address).await?;
850                eyre::ensure!(
851                    name == who,
852                    "Forward lookup verification failed: got `{name}`, expected `{who}`"
853                );
854            }
855            print_scalar(address.to_string())?;
856        }
857
858        // Misc
859        CastSubcommand::Keccak { data } => {
860            let bytes = match data {
861                Some(data) => data.into_bytes(),
862                None => stdin::read_bytes(false)?,
863            };
864            let out = match String::from_utf8(bytes) {
865                Ok(s) => SimpleCast::keccak(&s)?,
866                Err(e) => format!("0x{}", hex::encode(keccak256(e.as_bytes()))),
867            };
868            print_scalar(out)?;
869        }
870        CastSubcommand::HashMessage { message } => {
871            let message = stdin::unwrap(message, false)?;
872            let out = eip191_hash_message(message).to_string();
873            print_scalar(out)?;
874        }
875        CastSubcommand::SigEvent { event_string } => {
876            let event_string = stdin::unwrap_line(event_string)?;
877            let parsed_event = get_event(&event_string)?;
878            print_scalar(format!("{:?}", parsed_event.selector()))?;
879        }
880        CastSubcommand::LeftShift { value, bits, base_in, base_out } => {
881            let out = SimpleCast::left_shift(&value, &bits, base_in.as_deref(), &base_out)?;
882            print_scalar(out)?;
883        }
884        CastSubcommand::RightShift { value, bits, base_in, base_out } => {
885            let out = SimpleCast::right_shift(&value, &bits, base_in.as_deref(), &base_out)?;
886            print_scalar(out)?;
887        }
888        // TODO(json): multi-line source code or directory expansion, needs structured envelope
889        CastSubcommand::Source {
890            address,
891            directory,
892            explorer_api_url,
893            explorer_url,
894            etherscan,
895            flatten,
896        } => {
897            let config = etherscan.load_config()?;
898            let chain = config.chain.unwrap_or_default();
899            let api_key = config.get_etherscan_api_key(Some(chain));
900            match (directory, flatten) {
901                (Some(dir), false) => {
902                    SimpleCast::expand_etherscan_source_to_directory(
903                        chain,
904                        address,
905                        api_key,
906                        dir,
907                        explorer_api_url,
908                        explorer_url,
909                    )
910                    .await?
911                }
912                (None, false) => sh_println!(
913                    "{}",
914                    SimpleCast::etherscan_source(
915                        chain,
916                        address,
917                        api_key,
918                        explorer_api_url,
919                        explorer_url
920                    )
921                    .await?
922                )?,
923                (dir, true) => {
924                    SimpleCast::etherscan_source_flatten(
925                        chain,
926                        address,
927                        api_key,
928                        dir,
929                        explorer_api_url,
930                        explorer_url,
931                    )
932                    .await?;
933                }
934            }
935        }
936        CastSubcommand::Create2(cmd) => {
937            cmd.run()?;
938        }
939        CastSubcommand::Wallet { command } => command.run().await?,
940        CastSubcommand::Completions { shell } => {
941            generate(shell, &mut CastArgs::command(), "cast", &mut std::io::stdout())
942        }
943        CastSubcommand::Logs(cmd) => cmd.run().await?,
944        CastSubcommand::DecodeTransaction { tx, network } => {
945            let tx = stdin::unwrap_line(tx)?;
946            let decoded_tx = match network {
947                #[cfg(feature = "optimism")]
948                Some(NetworkVariant::Optimism) => {
949                    SimpleCast::decode_raw_transaction::<Optimism>(&tx)?
950                }
951                Some(NetworkVariant::Tempo) => {
952                    SimpleCast::decode_raw_transaction::<TempoNetwork>(&tx)?
953                }
954                _ => SimpleCast::decode_raw_transaction::<Ethereum>(&tx)?,
955            };
956            print_json_object(decoded_tx)?;
957        }
958        CastSubcommand::RecoverAuthority { auth } => {
959            let auth: SignedAuthorization = serde_json::from_str(&auth)?;
960            let out = auth.recover_authority()?.to_string();
961            print_scalar(out)?;
962        }
963        CastSubcommand::TxPool { command } => command.run().await?,
964        CastSubcommand::Erc20Token { command } => command.run().await?,
965        CastSubcommand::Tip20Token { command } => command.run().await?,
966        CastSubcommand::Keychain { command } => command.run().await?,
967        CastSubcommand::KeyAuth { command } => command.run().await?,
968        CastSubcommand::Tempo { command } => command.run().await?,
969        CastSubcommand::VirtualAddress { command } => command.run().await?,
970        #[cfg(feature = "optimism")]
971        CastSubcommand::DAEstimate(cmd) => {
972            cmd.run().await?;
973        }
974        CastSubcommand::Trace(cmd) => cmd.run().await?,
975    };
976
977    Ok(())
978}
979
980pub(crate) fn classify_raw_transaction_output(raw_tx: &str) -> Result<String> {
981    let raw_tx = hex::decode(raw_tx)?;
982    let mut data = raw_tx.as_slice();
983    let tx =
984        FoundryTxEnvelope::decode_2718(&mut data).wrap_err("failed to decode raw transaction")?;
985    format_lane_classification(&tx.classify_t5_payment_lane())
986}
987
988pub(crate) fn format_lane_classification(
989    classification: &PaymentLaneClassification,
990) -> Result<String> {
991    if shell::is_json() {
992        Ok(serde_json::to_string_pretty(classification)?)
993    } else {
994        Ok(serde_json::to_string(classification)?)
995    }
996}
997
998#[cfg(test)]
999mod tests {
1000    use super::*;
1001    use foundry_cli::introspect::{
1002        CommandRegistry, INTROSPECT_SCHEMA_ID, IntrospectDocument, OutputMode, build_document,
1003        capability_violations, duplicate_command_ids, render_introspect_document,
1004    };
1005
1006    /// Every `command_id` exposed by `cast --introspect` MUST be unique.
1007    /// This is the foundation of the agent contract — agents key on
1008    /// `command_id` to identify commands, and duplicates would silently break
1009    /// downstream tooling.
1010    ///
1011    /// Cast's clap tree is large and exhausts the default test-thread stack
1012    /// (2 MiB) when constructed in debug builds, so we spawn a worker thread
1013    /// with an explicit, generous stack size.
1014    #[test]
1015    fn introspect_command_ids_are_unique() {
1016        let dups = std::thread::Builder::new()
1017            .stack_size(16 * 1024 * 1024)
1018            .spawn(|| {
1019                let cmd = CastArgs::command();
1020                let doc = build_document(&cmd, &REGISTRY);
1021                duplicate_command_ids(&doc)
1022            })
1023            .expect("spawn worker thread")
1024            .join()
1025            .expect("worker thread join");
1026        assert!(dups.is_empty(), "duplicate cast command_ids: {dups:?}");
1027    }
1028
1029    /// `cast --introspect` must produce a JSON document that parses back into
1030    /// the canonical `IntrospectDocument` shape. Runs on a 16 MiB worker
1031    /// thread for the same reason as the uniqueness check above.
1032    #[test]
1033    fn introspect_document_is_valid_json() {
1034        let json = std::thread::Builder::new()
1035            .stack_size(16 * 1024 * 1024)
1036            .spawn(|| {
1037                let cmd = CastArgs::command();
1038                render_introspect_document(&cmd, &CommandRegistry::EMPTY)
1039            })
1040            .expect("spawn worker thread")
1041            .join()
1042            .expect("worker thread join");
1043        let doc: IntrospectDocument = serde_json::from_str(&json).expect("valid JSON");
1044        assert_eq!(doc.schema_id, INTROSPECT_SCHEMA_ID);
1045        assert_eq!(doc.binary.name, "cast");
1046    }
1047
1048    /// Capability self-consistency: any command declaring an output mode
1049    /// must wire the matching schema reference. See
1050    /// [`capability_violations`].
1051    #[test]
1052    fn introspect_capabilities_are_consistent() {
1053        let v = std::thread::Builder::new()
1054            .stack_size(16 * 1024 * 1024)
1055            .spawn(|| {
1056                let cmd = CastArgs::command();
1057                let doc = build_document(&cmd, &REGISTRY);
1058                capability_violations(&doc)
1059            })
1060            .expect("spawn worker thread")
1061            .join()
1062            .expect("worker thread join");
1063        assert!(v.is_empty(), "cast capability violations: {v:?}");
1064    }
1065
1066    /// Every adopted command (`output_mode = Envelope`) must pin a stable
1067    /// `command_id` matching its registry entry.
1068    #[test]
1069    fn registered_commands_pin_stable_ids() {
1070        let ids = std::thread::Builder::new()
1071            .stack_size(16 * 1024 * 1024)
1072            .spawn(|| {
1073                let cmd = <CastArgs as clap::CommandFactory>::command();
1074                let doc = build_document(&cmd, &REGISTRY);
1075                fn walk(c: &foundry_cli::introspect::CommandInfo) -> Vec<String> {
1076                    let mut out = Vec::new();
1077                    if matches!(c.capabilities.output_mode, OutputMode::Envelope) {
1078                        out.push(c.command_id.clone());
1079                    }
1080                    for sub in &c.subcommands {
1081                        out.extend(walk(sub));
1082                    }
1083                    out
1084                }
1085                doc.commands.iter().flat_map(walk).collect::<Vec<_>>()
1086            })
1087            .expect("spawn worker thread")
1088            .join()
1089            .expect("worker thread join");
1090        assert!(
1091            ids.iter().any(|s| s == "cast.call"),
1092            "cast.call missing from envelope ids: {ids:?}"
1093        );
1094    }
1095}